# imba$inlineHelpers=1 # imba$v2=0 # TODO Create Expression - make all expressions inherit from these? var helpers = require './helpers' var constants = require './constants' var fspath = require 'path' import ImbaParseError,ImbaTraverseError from './errors' import Token from './token' import SourceMap from './sourcemap' import StyleRule,StyleTheme,Color,StyleSheet,parseColorString from './styler.imba' import ReservedIdentifierRegex,InternalPrefixes,toJSIdentifier,toCustomTagIdentifier from '../utils/identifiers.imba' import Compilation from './compilation' import SourceMapper from './sourcemapper' var TAG_NAMES = constants.TAG_NAMES var TAG_GLOBAL_ATTRIBUTES = constants.TAG_GLOBAL_ATTRIBUTES var TAG_TYPES = {} var TAG_ATTRS = {} var EXT_LOADER_MAP = { svg: 'image' png: 'image' apng: 'image' jpg: 'image' jpeg: 'image' gif: 'image' tiff: 'image' bmp: 'image' } TAG_TYPES.HTML = "a abbr address area article aside audio b base bdi bdo big blockquote body br button canvas caption cite code col colgroup data datalist dd del details dfn div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hr html i iframe img input ins kbd keygen label legend li link main map mark menu menuitem meta meter nav noscript object ol optgroup option output p param pre progress q rp rt ruby s samp script section select small source span strong strike style sub summary sup table tbody td textarea tfoot th thead time title tr track u ul var video wbr".split(" ") TAG_TYPES.SVG = "circle defs ellipse g line linearGradient mask path pattern polygon polyline radialGradient rect stop svg text tspan".split(" ") TAG_ATTRS.HTML = "accept accessKey action allowFullScreen allowTransparency alt async autoComplete autoFocus autoPlay cellPadding cellSpacing charSet checked className cols colSpan content contentEditable contextMenu controls coords crossOrigin data dateTime defer dir disabled download draggable encType form formNoValidate frameBorder height hidden href hrefLang htmlFor httpEquiv icon id label lang list loop max maxLength mediaGroup method min multiple muted name noValidate pattern placeholder poster preload radioGroup readOnly rel required role rows rowSpan sandbox scope scrollLeft scrolling scrollTop seamless selected shape size span spellCheck src srcDoc srcSet start step style tabIndex target title type useMap value width wmode" TAG_ATTRS.SVG = "cx cy d dx dy fill fillOpacity fontFamily fontSize fx fy gradientTransform gradientUnits markerEnd markerMid markerStart offset opacity patternContentUnits patternUnits points preserveAspectRatio r rx ry spreadMethod stopColor stopOpacity stroke strokeDasharray strokeLinecap strokeOpacity strokeWidth textAnchor transform version viewBox x1 x2 x y1 y2 y" var CUSTOM_EVENTS = { intersect: 'events_intersect' selection: 'events_selection' resize: 'events_resize' hotkey: 'events_hotkey' touch: 'events_touch' pointer: 'events_pointer' pointerdown: 'events_pointer' pointermove: 'events_pointer' pointerover: 'events_pointer' pointerout: 'events_pointer' pointerup: 'events_pointer' pointercancel: 'events_pointer' lostpointercapture: 'events_pointer' click: 'events_mouse' mousedown: 'events_mouse' mouseup: 'events_mouse' mouseenter: 'events_mouse' mouseleave: 'events_mouse' mousemove: 'events_mouse' mouseout: 'events_mouse' mouseover: 'events_mouse' mousewheel: 'events_mouse' keydown: 'events_keyboard' keyup: 'events_keyboard' keypress: 'events_keyboard' } export var AST = {} var TREE_TYPE = DYNAMIC: 1 STATIC: 2 SINGLE: 3 OPTLOOP: 4 LOOP: 5 export var F = TAG_INITED: 2 ** 0 TAG_BUILT: 2 ** 1 # available TAG_CUSTOM: 2 ** 2 # available TAG_AWAKENED: 2 ** 3 TAG_MOUNTED: 2 ** 4 TAG_SCHEDULE: 2 ** 5 # available TAG_SCHEDULED: 2 ** 6 TAG_FIRST_CHILD: 2 ** 7 TAG_LAST_CHILD: 2 ** 8 TAG_HAS_DYNAMIC_FLAGS: 2 ** 9 TAG_HAS_BRANCHES: 2 ** 10 TAG_HAS_LOOPS: 2 ** 11 TAG_HAS_DYNAMIC_CHILDREN: 2 ** 12 TAG_IN_BRANCH: 2 ** 13 TAG_BIND_MODEL: 2 ** 14 TAG_INDEXED: 2 ** 15 # not used TAG_KEYED: 2 ** 16 # not used EL_INITED: 2 ** 0 EL_HYDRATED: 2 ** 1 EL_HYDRATING: 2 ** 2 EL_AWAKENED: 2 ** 3 EL_MOUNTING: 2 ** 4 EL_MOUNTED: 2 ** 5 EL_SCHEDULE: 2 ** 6 # available EL_SCHEDULED: 2 ** 7 EL_RENDERING: 2 ** 8 EL_RENDERED: 2 ** 9 EL_SSR: 2 ** 10 EL_TRACKED: 2 ** 11 # emit mount/unmount events EL_SUSPENDED: 2 ** 12 # block commit from rendering EL_UNRENDERED: 2 ** 13 # render marks DIFF_BUILT: 2 ** 0 DIFF_FLAGS: 2 ** 1 DIFF_ATTRS: 2 ** 2 DIFF_CHILDREN: 2 ** 3 DIFF_MODIFIERS: 2 ** 4 DIFF_INLINE: 2 ** 5 # Helpers for operators export var OP = do |op, l, r| var o = String(op) switch o when '.','?.' if l isa Super and !l.member l.member = r return l r = Identifier.new(r) if r isa String # r = r.value if r isa VarOrAccess # if r.@value and r.@value.@value == 'new' # # TODO remove support for this # return New.new(l).set(keyword: r) Access.new(op,l,r) when '=' Assign.new(op,l,r) when '~=' return OP('&=',l,OP('~',r)) when '||=','&&=','??=' ConditionalAssign.new(op,l,r) when '+=','-=','*=','/=','^=','%=','**=' CompoundAssign.new(op,l,r) # when '?.' # if r isa VarOrAccess # r = r.value # # depends on the right side - this is wrong # PropertyAccess.new(op,l,r) when 'instanceof','isa' InstanceOf.new(op,l,r) when 'in' In.new(op,l,r) when 'typeof' TypeOf.new(op,l,r) when 'delete' Delete.new(op,l,r) when '--','++','!','√','not','!!' UnaryOp.new(op,l,r) when '>','<','>=','<=','==','===','!=','!==' ComparisonOp.new(op,l,r) when '..','...' Range.new(op,l,r) else Op.new(op,l,r) var PATHIFY = do |val| # console.log "PATHIFY {val}" if val isa TagAttrValue val = val.value if val isa ArgList val = val.values[0] while val isa Parens val = val.value if val isa VarOrAccess val = val.@variable or val.value if val isa Access let left = val.left let right = val.right isa Index ? val.right.value : val.right if left isa VarOrAccess left = left.@variable or left.value if right isa VarOrAccess right = right.@variable or right.value if val isa IvarAccess left ||= val.scope__.context if right isa SymbolIdentifier yes elif right isa Identifier right = helpers.singlequote(String(right.js)) right = Str.new(right) return [left,right] return val var OPTS = {} var ROOT = null export var NODES = [] var C = do |node,opts| (typeof node == 'string' or typeof node == 'number') ? node : node.c(opts) var MLOC = do |a,b| b = a if b == undefined { startLoc: do a endLoc: do b } var M = do |val,mark,o| if mark == undefined mark = val if mark and mark:startLoc val = C(val,o) let ref = STACK.incr('sourcePair') let start = mark.startLoc let end = mark.endLoc let m0 = '' let m1 = '' if start == 0 or start > 0 m0 = end >= start ? "/*%{start}|{ref}$*/" : "/*%{start}$*/" if end == 0 or end > 0 m1 = start >= 0 ? "/*%{end}|{ref}$*/" : "/*%{end}$*/" return m0 + val + m1 return C(val,o) var MSTART = do |*params| for item in params if item isa Number return item if item and item:startLoc isa Function return item.startLoc return null var MEND = do |*params| for item in params if item isa Number return item if item and item:endLoc isa Function return item.endLoc return null var TYP = do |item,format| let typ = item.@options and item.@options:datatype typ ||= item:datatype isa Function and item.datatype if typ let str = C(typ) if format == 'jsdoc' "/** @type \{{M(str,typ)}\} */" else ":{M(str,typ)}" else "" var LIT = do |val| RawScript.new(val) var SYM = do |val| Symbol.new(val) var KEY = do |val| val = val.value if val isa Token if val isa String if val.match(/^[a-zA-Z\$\_]+[\d\w\$\_]*$/) val = Identifier.new(val) else val = Str.new(helpers.singlequote(String(val))) return val var STR = do |val| return val if val isa Str return Str.new(helpers.singlequote(String(val))) var IF = do |cond,body,alt,o={}| var node = If.new(cond,body,o) node.addElse(alt) if alt node var NODIFY = do |val| if val == null return Nil.new elif val == false return False.new elif val == true return True.new elif val isa String return STR(val) elif val isa Number return Num.new(val) else return val var FN = do |pars,body,scope| let fn = Func.new(pars,body) if scope fn.@scope.@systemscope = scope return fn var METH = do |pars,body| ClosedFunc.new(pars,body) var CALL = do |callee,pars = []| # possibly return instead(!) Call.new(callee,pars) var GET = do |left,right| OP('.',left,right) var CALLSELF = do |name,pars = []| var ref = Identifier.new(name) Call.new(OP('.',SELF,ref),pars) var BLOCK = do Block.wrap([]:slice.call(arguments)) var WHILE = do |test,code| While.new(test).addBody(code) export var SPLAT = do |value| Splat.new(value) # if value isa Assign # value.left = Splat.new(value.left) # return value # else # Splat.new(value) var SEMICOLON_TEST = /;(\s*\/\/.*)?[\n\s\t]*$/ var RESERVED_TEST = /^(default|char|for)$/ # captures error from parser export def parseError str, o var err = Compilation.error({ category: 'parser' severity: 'error' offset: o:offset length: o:length message: str }) return err.raise() def AST.c obj typeof obj == 'string' ? obj : obj.c def AST.compileRaw item let o = '' if item isa Array o = '[' for v in item o += AST.compileRaw(v) + ',' o = o.slice(0,-1) + ']' elif item isa Object o = '{ ' for own k,v of item # maybe quote? o += "{k}: {AST.compileRaw(v)}," o = o.slice(0,-1) + ' }' else o = JSON.stringify(item) return o def AST.blk obj obj isa Array ? Block.wrap(obj) : obj def AST.sym obj # console.log "sym {obj}" helpers.symbolize(String(obj),STACK) def AST.cary ary,params = null ary.map do |v| if typeof v == 'string' return v elif v && v:c return params ? v.c(params) : v.c else # console.warn 'could not compile',v return String(v) def AST.dump obj, key if obj isa Array obj.map do |v| v && v:dump ? v.dump(key) : v elif obj and obj:dump obj.dump def AST.compact ary if ary isa ListNode return ary.compact ary.filter do |v| v != undefined && v != null def AST.reduce res,ary for v in ary v isa Array ? AST.reduce(res,v) : res.push(v) return def AST.flatten ary, compact = no var out = [] for v in ary v isa Array ? AST.reduce(out,v) : out.push(v) return out def AST.loc item if !item [0,0] elif item isa Token item.region elif item isa Node item.loc def AST.parse str, opts = {} var indent = str.match(/\t+/)[0] # really? Require the compiler, not this Imbac.parse(str,opts) def AST.inline str, opts = {} parse(str,opts).body def AST.node typ, pars if typ == 'call' if pars[0].c == 'return' pars[0] = 'tata' Call.new(pars[0],pars[1],pars[2]) def AST.escapeComments str return '' unless str return str var shortRefCache = [] def AST.counterToShortRef nr var base = "A".charCodeAt(0) nr += 30 while shortRefCache:length <= nr var num = shortRefCache:length + 1 var str = "" while true num -= 1 str = String.fromCharCode(base + (num % 26)) + str num = Math.floor(num / 26) break unless num > 0 shortRefCache.push(str.toLowerCase()) return shortRefCache[nr] def AST.truthy node if node isa True return true if node isa False return false if node:isTruthy return node.isTruthy return undefined export class Indentation prop open prop close def initialize a,b @open = a @close = b self def isGenerated @open and @open:generated def aloc @open and @open.@loc or 0 def bloc @close and @close.@loc or 0 def startLoc aloc def endLoc bloc def wrap str var om = @open and @open.@meta var pre = om and om:pre or '' var post = om and om:post or '' var esc = AST:escapeComments var out = @close # the first newline should not be indented? str = post.replace(/^\n/,'') + str str = str.replace(/^/g,"\t").replace(/\n/g,"\n\t").replace(/\n\t$/g,"\n") str = pre + '\n' + str str += out.c if out isa Terminator str = str + '\n' unless str[str:length - 1] == '\n' return str var INDENT = Indentation.new({},{}) class Stash def initialize @entities = [] def add item @entities.unshift(item) self def pluck item var match = null for entity,i in @entities if entity == item or entity isa item match = entity @entities.splice(i,1) return match return null export class Stack prop loglevel prop nodes prop scopes prop root prop state prop meta prop theme prop css def initialize reset def reset @nodes = [] @scoping = [] @scopes = [] @stash = Stash.new(self) @loglevel = 3 @counter = 0 @counters = {} @options = {} @state = {} @tag = null @sourceId = null @symbols = {} @css = StyleSheet.new(self) @theme = null @meta = {} # @css = '' @runtime self def runtime @root.runtime def cssns @root.cssns def use item @root.use(item) def incr name @counters[name] ||= 0 @counters[name] += 1 def decr name @counters[name] ||= 0 @counters[name] -= 1 def strip val SourceMapper.strip(val) def generateId ns = 'oid' AST.counterToShortRef(STACK.incr(ns)) def getSymbol ref, alias = null, name = '' let key = ref or incr('symbols') # ref ||= "" + incr('symbols') # Belongs in root @symbols[key] ||= @root.declare((alias or ref),LIT("Symbol({name ? helpers.singlequote(name) : ''})"),system: yes, alias: (alias or ref)).resolve.c def symbolFor name @root.symbolRef(name) def toInternalClassName name if name:toClassName name = name.toClassName elif name:c isa Function name = name.c let str = "Ω" + strip(name).split(".").join("__") let nr = incr(str) if nr > 1 str += "Ω" + nr return str def domCall name if true name = { start: 'beforeVisit' end: 'afterVisit' open: 'beforeReconcile' close: 'afterReconcile' insert: 'placeChild' }[name] or name "[{symbolFor('#' + name)}]" else ".{name}$" def sourceId return @sourceId if (@sourceId ||= @options:sourceId) let src = sourcePath let cwd = self.cwd # relativize the cwd thing # TODO rename+document this option. sourceBase or sourceRoot? if @options:path and cwd src = @options:path.relative(cwd,src) unless src throw Error.new("Include sourceId or sourcePath in options compile(code,options)") @sourceId = helpers.identifierForPath(src) return @sourceId def theme @theme ||= StyleTheme.wrap(@options:config) def stash @stash def set obj @options ||= {} for own k,v of obj @options[k] = v self # get and set def option key, val if val != undefined @options ||= {} @options[key] = val return self @options && @options[key] def platform @options:platform or 'browser' def format @options:format def sourcePath @options:sourcePath def imbaPath @options:imbaPath def resolveColors @options:styles !== 'extern' or @options:resolveColors def config @options:config or {} def cwd config && config:cwd def tsc platform == 'tsc' or @options:tsc def hmr !!@options:hmr def isStdLib !!@options:stdlib def isWeb platform == 'browser' or platform == 'web' def isWorker platform == 'worker' def isNode platform == 'node' def env key var val = @options["ENV_{key}"] return val if val != undefined if F[key] !== undefined return F[key] var lowercased = key.toLowerCase if @options[lowercased] != undefined return @options[lowercased] if key == 'WEB' or key == 'BROWSER' @meta:universal = no return isWeb elif key == 'NODE' @meta:universal = no return isNode elif key == 'WORKER' or key == 'WEBWORKER' @meta:universal = no return isWorker elif key == 'HMR' return !!@options:hmr if var e = @options:env if e.hasOwnProperty(key) return e[key] elif e.hasOwnProperty(key.toLowerCase) return e[key.toLowerCase] if $node$ and typeof process != 'undefined' and process:env val = process:env[key.toUpperCase] if val != undefined return val return null return undefined def addScope scope @scopes.push(scope) self def traverse node self def push node @nodes.push(node) # not sure if we have already defined a scope? self def pop node @nodes.pop # (node) self def parent @nodes[@nodes:length - 2] def current @nodes[@nodes:length - 1] def up test test ||= do |v| !(v isa VarOrAccess) if typeof test == 'number' return @nodes[@nodes:length - (1 + test)] var i = @nodes:length - 2 if test:prototype isa Node while i >= 0 var node = @nodes[i--] return node if node isa test return null while i >= 0 var node = @nodes[i] return node if test(node) i -= 1 return null def parents test test ||= do |v| !(v isa VarOrAccess) if test:prototype isa Node let cls = test test = do |v| v isa cls @nodes.filter(test) def relative node, offset = 0 var idx = @nodes.indexOf(node) idx >= 0 ? @nodes[idx + offset] : null def scope lvl = 0 return @withScope if @withScope var i = @nodes:length - 1 - lvl while i >= 0 var node = @nodes[i] return node.@scope if node.@scope i -= 1 return null def withScope scop, cb let prev = @withScope @withScope = scop cb() @withScope = prev return def scopes # include deeper scopes as well? var scopes = [] var i = @nodes:length - 1 while i >= 0 var node = @nodes[i] scopes.push(node.@scope) if node.@scope i -= 1 return scopes def closures scopes.filter do |scope| scope.closure == scope def method up(MethodDeclaration) def block up(Block) def blockpart let i = @nodes:length - 1 while i if @nodes[i - 1] isa Block return @nodes[i] i-- return def lastImport let scopes = self.scopes for scope in scopes if scope.@lastImport return scope.@lastImport return null def isExpression var i = @nodes:length - 1 while i >= 0 var node = @nodes[i] # why are we not using isExpression here as well? if node isa Code or node isa Loop or node.isStatementLike return false if node.isExpression return true # probably not the right test - need to be more explicit i -= 1 return false def toString "Stack({@nodes.join(" -> ")})" def isAnalyzing @analyzing def scoping @nodes.filter(|n| n.@scope ).map(|n| n.@scope ) def currentRegion let l = @nodes:length let node = @nodes[--l] node and [node.startLoc,node.endLoc] # Lots of globals -- really need to deal with one stack per file / context export var STACK = Stack.new # use a bitmask for these export class Node prop o prop options prop traversed # reference to the script object this node # is part of def script # TODO don't use global state for this Compilation:current def safechain no def oid @oid ||= STACK.generateId('') def osym ns = '',name = '' STACK.getSymbol(oid + ns,null,name) def symbolRef name STACK.root.symbolRef(name) def domCall name STACK.domCall(name) # get global symbol with name def gsym name STACK.root.symbolRef(name) def sourceId STACK.sourceId # shorthand for the self context for a node def slf scope__.context def p # allow controlling this from CLI if STACK.loglevel > 0 console.log(*arguments) self def runtime STACK.runtime def typeName self:constructor:name def namepath typeName def initialize setup self def setup @expression = no @traversed = no @parens = no @cache = null @value = null self def setStartLoc loc @startLoc = loc return self def setEndLoc loc @endLoc = loc return self def setRegion loc if loc isa Node loc = loc.region if loc isa Array @startLoc = loc[0] @endLoc = loc[1] return self def setEnds start,end if end and end:endLoc @endLoc = end.endLoc if start and start:startLoc @startLoc = start.startLoc self def startLoc @startLoc def endLoc @endLoc def set obj @options ||= {} for own k,v of obj @options[k] = v self # get and set def option key, val if val != undefined @options ||= {} @options[key] = val return self @options && @options[key] def o @options ||= {} def keyword @keyword or (@options and @options:keyword) def datatype @options ? @options:datatype : null def configure obj set(obj) def region [0,0] def loc [startLoc or 0,endLoc or 0] def token null def compile self def visit self def stack STACK def isString no def isPrimitive deep no def isReserved no def isGlobal name no def isConstant no # should rather do traversals # o = {}, up, key, index def traverse o if @traversed return self # NODES.push(self) @traversed = yes let prev if o prev = STACK.state STACK.state = o STACK.push self visit(STACK,STACK.state) STACK.pop self if o STACK.state = prev return self def inspect {type: self:constructor.toString} def js o "NODE" def toString "{self:constructor:name}" # swallow might be better name def consume node if node isa TagLike return node.register(self) if node isa PushAssign node.register(self) return PushAssign.new(node.op,node.left,self) if node isa Assign # node.right = self return OP(node.op,node.left,self) elif node isa VarDeclaration return OP('=',node.left,self) elif node isa Op return OP(node.op,node.left,self) elif node isa Return return Return.new(self) elif node == NumberLike return NumberLike.new(self) return self def toExpression @expression = true self def forceExpression @expression = true self def isExpressable true def isExpression @expression || false def isStatementLike false def isRuntimeReference no def hasSideEffects true def isUsed true def shouldParenthesize false def shouldParenthesizeInTernary yes def block Block.wrap([self]) def node self def unwrappedNode self def scope__ STACK.scope def up STACK.parent def util Util def receiver self def indented a,b if a isa Indentation @indentation = a return self # this is a _BIG_ hack if b isa Array add(b[0]) b = b[1] # if indent and indent.match(/\:/) @indentation ||= a and b ? Indentation.new(a,b) : INDENT self def prebreak term = '\n' self def invert return OP('!',self) def cache o = {} @cache = o o:var = (o:scope or scope__).temporary(self,o) o:lookups = 0 self def cachevar @cache && @cache:var def decache if @cache cachevar.free @cache = null self # the "name-suggestion" for nodes if they need to be cached def alias null def warn message, opts = {} let loc = opts:loc or self.loc or [0,0] if loc isa Node loc = [loc.startLoc,loc.endLoc] if loc isa Token loc = loc.loc # if loc[0] == 0 and loc[1] == 0 # console.log 'loc warn',loc,script.rangeAt(loc[0],loc[1]) script.addDiagnostic(opts:severity or 'warning',{ message: message range: script.rangeAt(loc[0],loc[1]) }) # Compilation.warn( # severity: opts:severity or 'warning' # message: message # offset: (loc ? loc[0] : 0) # length: (loc ? (loc[1] - loc[0]) : 0) # ) def error message, opts = {} opts:severity = 'error' warn(message,opts) def c o var s = STACK var ch = @cache return c_cached(ch) if ch and ch:cached s.push(self) forceExpression if o && o:expression if o and o:indent @indentation ||= INDENT var out = js(s,o) var paren = shouldParenthesize s.pop(self) if out == undefined return out if var indent = @indentation out = indent.wrap(out,o) self # should move this somewhere else really out = "({out})" if paren if (o and o:braces) or (@options and @options:braces) if indent out = '{' + out + '}' else out = '{ ' + out + ' }' if ch = @cache out = "{ch:var.c} = {out}" unless ch:manual var par = s.current par = par.node if par isa ValueNode out = '(' + out + ')' if par isa Access || par isa Op # others? # ch:cached = yes if OPTS:sourcemap and (!o or o:mark !== false) out = M(out,self) return out def c_cached cache cache:lookups++ cache:var.free if cache:uses == cache:lookups return cache:var.c # recompile every time?? export class ValueNode < Node prop value def startLoc @value?.startLoc def endLoc @value?.endLoc def initialize value setup @value = load(value) def load value value def js o typeof @value == 'string' ? @value : @value.c def visit @value.traverse if @value isa Node # && @value:traverse self def region [@value.@loc,@value.@loc + @value.@len] export class ValueReferenceNode < Node prop value prop orig def initialize value, orig setup @value = value @orig = orig or value def startLoc @orig?.startLoc def endLoc @orig?.endLoc def load value value def js o let res = M(@value.c(mark: false),self) return res def visit @value.traverse if @value isa Node # && @value:traverse self def region [@orig.@loc,@orig.@loc + @orig.@len] export class ExpressionNode < ValueNode export class AssertionNode < ValueNode def js o let op = @value let out = [] if op isa Op and !(op isa Access) let l = op.left let r = op.right out.push(l.cache.c(o)) out.push(helpers.singlequote(op.@op)) out.push(r.cache.c(o)) out = ["imba.$a=[{out.join(',')}]"] out.push(op.c(o)) # op.cache # out.push('imba.$asrt=undefined') # out.push(op.c(o)) else out.push('imba.$a=null') out.push(op.c(o)) return '(' + out.join(',') + ")" # ,{op.c(o)}) # "('assert',{super})" export class Statement < ValueNode def isExpressable return no export class Meta < ValueNode def isPrimitive deep yes export class Comment < Meta def visit if var block = up var idx = block.indexOf(self) + 1 idx += 1 if block.index(idx) isa Terminator if var next = block.index(idx) next.@desc = self self def toDoc helpers.normalizeIndentation("" + @value.@value) def toJSON helpers.normalizeIndentation("" + @value.@value) def toString @value.@value def c o return "" if STACK.option(:comments) == false or @skip var v = @value.@value if o and o:expression or v.match(/\n/) or @value.type == 'HERECOMMENT' # multiline? var out = v.replace(/\*\//g, '\\*\\/').replace(/\/\*/g, '\\/\\*') "/*{out}*/" elif v.match(/\@(type|param)/) "/** {v} */" else "// {v}" export class Terminator < Meta def initialize v @value = v self def traverse self def loc [@value.@loc,@value.@loc + @value.@value:length] def startLoc @value:startLoc ? @value.startLoc : -1 def endLoc @value.@value ? (startLoc + @value.@value:length) : -1 def c let val = @value.c if STACK.option(:comments) == false val = val.replace(/\/\/.*$/gm,'') if STACK.tsc # make comments significant for tooling # temporary hack to work around parsing issue with reference path val = val.replace(/\/{3}/g,'~~/~~') val = val.replace(/\/\/\s(.*)$/gm,'/** $1 */ ') val = val.replace(/\~\~\/\~\~/g,'///') if STACK.tsc and (val:length > 1 or @first) return M(val.replace(/^[\t ]+/gm,''),self) return val.replace(/^[\t ]+/gm,'') export class Newline < Terminator def initialize v @traversed = no @value = v or '\n' def c @value # M(@value,@value) # weird place? export class Index < ValueNode def cache o = {} @value.cache(o) def js o @value.c export class ListNode < Node prop nodes def initialize list setup @nodes = load(list or []) @indentation = null # PERF acces @nodes directly? def list @nodes def compact @nodes = AST.compact(@nodes) self def load list list def concat other # need to store indented content as well? @nodes = nodes.concat(other isa Array ? other : other.nodes) self def swap item, other var idx = indexOf(item) nodes[idx] = other if idx >= 0 self def push item @nodes.push(item) self def pop var end = @nodes.pop return end def add item, o let idx = null if o and o:before idx = @nodes.indexOf(o:before) idx = null if idx == -1 elif o and o:after idx = @nodes.indexOf(o:after) + 1 idx = null if idx == 0 if idx >= 1 while @nodes[idx] isa Meta idx++ elif o isa Number idx = o if idx !== null item isa Array ? @nodes.splice(idx,0,*item) : @nodes.splice(idx,0,item) else item isa Array ? @nodes.push(*item) : @nodes.push(item) self def unshift item, br @nodes.unshift(BR) if br @nodes.unshift(item) self # test def slice a, b self:constructor.new(@nodes.slice(a,b)) def break br, pre = no br = Terminator.new(br) if typeof br == 'string' pre ? unshift(br) : push(br) self def some cb for node in @nodes return yes if cb(node) return no def every cb for node in @nodes return no unless cb(node) return yes # filtered list of items def values @nodes.filter do |item| !(item isa Meta) def filter cb @nodes.filter(cb) def pluck cb var item = filter(cb)[0] remove(item) if item return item def indexOf item @nodes.indexOf(item) def index i @nodes[i] def remove item var idx = @nodes.indexOf(item) @nodes.splice(idx, 1) if idx >= 0 self def removeAt idx var item = @nodes[idx] @nodes.splice(idx, 1) if idx >= 0 return item def replace original, replacement var idx = @nodes.indexOf(original) if idx >= 0 if replacement isa Array @nodes.splice(idx,1,*replacement) else @nodes[idx] = replacement self def first @nodes[0] def last var i = @nodes:length while i i = i - 1 var v = @nodes[i] return v unless v isa Meta return null def map fn @nodes.map(fn) def forEach fn @nodes.forEach(fn) def remap fn @nodes = map(fn) self def count @nodes:length def len @nodes:length def realCount var k = 0 for node in @nodes k++ if node and !(node isa Meta) return k def isEmpty realCount == 0 def visit let items = @nodes let i = 0 while i < items:length let item = items[i] if item:traverse let res = item.traverse if res != item if res isa Array items.splice(i,1,*res) continue i++ self def isExpressable for node in nodes return no if node and !node.isExpressable return yes def toArray @nodes def delimiter @delimiter or "," def js o, nodes: @nodes var delim = ',' var express = delim != ';' var last = last var i = 0 var l = nodes:length var str = "" for arg in nodes var part = typeof arg == 'string' ? arg : (arg ? arg.c(expression: express) : '') str += part str += delim if part and (!express or arg != last) and !(arg isa Meta) return str def indented a,b if a isa Indentation @indentation = a return self @indentation ||= a and b ? Indentation.new(a,b) : INDENT self def endLoc if @endLoc return @endLoc var i = @nodes:length let last = @nodes[i - 1] last?.endLoc export class ArgList < ListNode def startLoc if typeof @startLoc == 'number' return @startLoc first?.startLoc def consume node if node isa TagLike @nodes = @nodes.map do |child| if !(child isa Meta) # and !(child isa Assign) child.consume(node) else child return self super def setEnds start,end @generated = start and start:generated if end and end:endLoc and end.endLoc != -1 @endLoc = end.endLoc if start and start:startLoc and start.startLoc != -1 @startLoc = start.startLoc self export class AssignList < ArgList def concat other if @nodes:length == 0 and other isa AssignList return other else super(other) # need to store indented content as well? # @nodes = nodes.concat(other isa Array ? other : other.nodes) self export class Block < ListNode prop head def initialize list setup @nodes = list or [] @head = null @indentation = null def startLoc @indentation ? @indentation.startLoc : super def endLoc @indentation ? @indentation.endLoc : super def self.wrap ary unless ary isa Array throw SyntaxError.new("what") ary:length == 1 && ary[0] isa Block ? ary[0] : Block.new(ary) def visit stack @scope.visit if @scope if stack and stack.@tag @tag = stack.@tag @traversing = true for node,i in @nodes.slice(0) node and node.traverse @traversing = false self def block self def collectDecorators if let decorators = @decorators @decorators = null return decorators return null def loc # rather indents, no? if var opt = option(:ends) var a = opt[0].loc var b = opt[1].loc p "no loc for {opt[0]}" unless a p "no loc for {opt[1]}" unless b return [a[0],b[1]] if var ind = @indentation if ind.aloc != -1 return [ind.aloc,ind.bloc] let a = @nodes[0] let b = @nodes[@nodes:length - 1] [a and a.loc[0] or 0,b and b.loc[1] or 0] # go through children and unwrap inner nodes def unwrap var ary = [] for node,i in nodes if node isa Block ary:push.apply(ary,node.unwrap) else ary.push(node) return ary # This is just to work as an inplace replacement of nodes.coffee # After things are working okay we'll do bigger refactorings def compile o = {} var root = Root.new(self,o) root.compile(o) # Not sure if we should create a separate block? def analyze o = {} self def cpart node return "" if node === BR0 var out = typeof node == 'string' ? node : (node ? node.c : "") return "" if out == null or out == undefined or out == "" if out isa Array var str = "" var l = out:length var i = 0 while i < l str += cpart(out[i++]) return str var hasSemiColon = SEMICOLON_TEST.test(out) out += delimiter unless hasSemiColon or node isa Meta return out def delimiter @delimiter == undefined ? ';' : @delimiter def js o, opts var ast = @nodes var l = ast:length # really? var express = isExpression or o.isExpression or (option(:express) and isExpressable) return '' if ast:length == 0 and (!@head or @head:length == 0) if express return super(o,nodes: ast) var str = "" let empty = no for v in ast let vs = cpart(v) # FIXME windows? if vs[0] == '\n' and (/^\n+$/).test(vs) continue if empty empty = yes elif vs empty = no # console.log 'add item?',JSON.stringify(vs) str += vs # now add the head items as well if @head and @head:length > 0 var prefix = "" for v in @head var hv = cpart(v) prefix += hv + '\n' if hv str = prefix + str if option(:strict) str = cpart('"use strict";\n') + str return str # Should this create the function as well? def defers original, replacement var idx = @nodes.indexOf(original) @nodes[idx] = replacement if idx >= 0 var rest = @nodes.splice(idx + 1) return rest def expressions var expressions = [] for node in nodes expressions.push(node) unless node isa Terminator return expressions def consume node if node isa TagLike let real = expressions @nodes = @nodes.map do |child| if child in real and !(child isa Assign) child.consume(node) else child return self # can also return super if it is expressable, but should we really? if var before = last var after = before.consume(node) if after != before if after isa Block after = after.nodes replace(before,after) return self def isExpressable return no unless @nodes.every(|v| v.isExpressable ) return yes def isExpression option(:express) || @expression def shouldParenthesizeInTernary if count == 1 return first.shouldParenthesizeInTernary yes def indented a,b super if a isa Token and a.@type == 'INDENT' if let post = (a.@meta and a.@meta:post) let br = Token.new('TERMINATOR',post) @nodes.unshift(Terminator.new(br)) a.@meta:post = '' return self class ClassInitBlock < Block def c o # @options ||= {} # @options:braces = yes let out = super return 'static {\n' + helpers.indent(out) + '}' # def js o, opts # let out = super(o,opts) # return 'static {\n' + out + '}' class InstanceInitBlock < Block class InstancePatchBlock < InstanceInitBlock export class ClassField < Node prop name def initialize name super @name = name def visit @decorators = up?.collectDecorators @classdecl = STACK.up(ClassDeclaration) @name.traverse if @name and @name:traverse if value value.@scope = @vscope = FieldScope.new(value) value.@scope.@parent = scope__ value.traverse if watchBody @descriptor = STACK.root.declare("{oid}$Prop",util.watcher(storageSymbol,watcherSymbol), type: 'const', system: yes) if wrapper # worth of a full function @vslot = osym('slot',String(@name)) @fslot = osym('meta') @fname = @name.metaIdentifier # let body = [] # let fn = @accessor = MethodDeclaration.new([],[LIT('"ets"'),CodeBlock.new([wrapper])],Identifier.new('render__'),null,{}) # let block = fn.body.add(CodeBlock.new([wrapper])) # let desc = @accessor.scope.declare('desc',wrapper) # console.log "wrapper {wrapper}" # @accessor.body.add(Return.new(desc)) # if STACK.tsc # body = [OP('=')] # fn.traverse # console.log fn.c wrapper.@scope = @vscope = FieldScope.new(wrapper) wrapper.@scope.@parent = scope__ wrapper.traverse # @vslot = osym('meta') # @getter = CALL(OP('.',THIS,@name),[]) self def value option(:value) def target option(:static) ? LIT('this') : LIT('this.prototype') def storageSymbol symbolRef("#{name.c(as:'symbolpart')}") def watcherSymbol symbolRef("#{name.c(as:'symbolpart')}DidSet") def storageKey @storageKey ||= STR(name.c + '$$') def storageMap @storageMap ||= scope__.root.declare(null,LIT('new WeakMap()')) def isPlain !@decorators and (!@value or @value.isPrimitive) def isMember !option(:static) def isLazy no def hasStaticInits isStatic or @decorators # or watchBody def hasConstructorInits !isStatic def isStatic option(:static) def watchBody option(:watch) def wrapper option(:wrapper) def loc [@name.@loc,@name.region[1]] def c return if option(:struct) let up = STACK.current let out if up isa ClassBody # return if isPlain let prefix = isStatic ? "{M('static',option(:static))} " : '' let name = name isa IdentifierExpression ? name.asObjectKey : name.c(as: 'field') let cls = STACK.up(ClassDeclaration) if wrapper let meta = @metaname = @name.metaIdentifier let slot = @vslot let metasym = @fslot let inner let context = null if isStatic context = cls.classReference.c else context = "{cls.classReference.c}.prototype" let op = OP('.',LIT('this'),meta) let args = "this,{slot},{@name.c(as: 'value')}" @getter = LIT("()\{ return {op.c}.$get({args}) \}") @setter = LIT("(val)\{ {op.c}.$set(val,{args}) \}") if STACK.tsc inner = "return {runtime:accessor}({wrapper.c(expression: yes)},{args},{metasym},{context})" else inner = "return this[{metasym}] || {runtime:accessor}({wrapper.c(expression: yes)},{args},{metasym},{context})" # inner = STACK.tsc ? "return {inner}" : "return this[{metasym}] || {inner}" @handler = LIT("{M(meta.c(as: 'field'),@name)}()\{ {inner} \}") if STACK.tsc # console.log 'here',STACK.tsc if wrapper let setter = "{prefix}set {M(name,@name)}{self.setter.c(keyword: '')}" let getter = "{prefix}get {M(name,@name)}{self.getter.c(keyword: '')}" if datatype getter = "{datatype.c} {getter}" out = "{setter}\n{getter}\n{prefix}get {@handler.c}" if !isStatic # how would this fare with class extensions? out += "\nstatic get {M(@metaname.c(as: 'field'),@name)}()\{ return {OP('.',LIT('this.prototype'),@metaname).c} \}" return out if self isa ClassAttribute or (@decorators and @decorators:length) let val = value ? value.c : '' let field = '' let getval = 'null' let setval = '' let sym = osym out = "{prefix} get {M(name,@name)}() \{ return /** @type any */({getval}) \}" # the value scope? if datatype out = "{datatype.c} {out}" out += "\n{prefix} set {M(name,@name)}(val) \{ {setval} \}" else out = "{prefix}{M(name,@name)}" # the value scope? out += " = {value.c};" if value let typ = datatype if typ out = "{typ.c} {out}" elif self isa ClassAttribute or (@decorators && @decorators:length > 0 and false) or wrapper let setter = "{prefix}set {name}{self.setter.c(keyword: '')}" let getter = "{prefix}get {name}{self.getter.c(keyword: '')}" out = "{setter}\n{getter}" if wrapper # let wr = "{runtime:property}({wrapper.c}).accessor()" out += "\n{prefix}get {@handler.c}" return out return if STACK.tsc if isStatic and up isa ClassInitBlock if @vscope if let fn = STACK.up(Func) @vscope.mergeScopeInto(fn.@scope) out = OP('=',OP('.',THIS,name),value or UNDEFINED).c + ';\n' elif !isStatic and up isa ClassInitBlock return "" let key = name let tpl = """ Object.defineProperty(this.prototype,{key.c(as: 'value')},\{ enumerable: true, set{self.setter.c(keyword: '')}, get{self.getter.c(keyword: '')} \}) """ return tpl elif !isStatic and up isa InstanceInitBlock if @vscope if let fn = STACK.up(Func) @vscope.mergeScopeInto(fn.@scope) let key = name if name isa Identifier key = name.toStr let ctor = up.option(:ctor) let opts = up.option(:opts) let val = value or UNDEFINED let paramIndex = option(:paramIndex) let restIndex = option(:restIndex) let access if up isa InstancePatchBlock let rest = ctor.@params.at(restIndex,yes,'$$',LIT('{}')) access = OP('.',rest,name) access.cache(reuse: yes, name: 'v') let right = OP('=',OP('.',THIS,name),access) out = OP('&&',OP('!==',access,UNDEFINED),right) elif paramIndex != undefined let name = option(:paramName) access = ctor.@params.at(paramIndex,yes,name) if value val = If.ternary(OP('!==',access,UNDEFINED),access,val) else val = access elif restIndex != undefined let rest = ctor.@params.at(restIndex,yes,'$$',LIT('null')) access = OP('.',rest,name) if value access.cache(reuse: yes, name: 'v') val = If.ternary(OP('&&',rest,OP('!==',access,UNDEFINED)),access,val) else val = If.ternary(rest,access,UNDEFINED) if self isa ClassAttribute and !value return if wrapper out = CALL( OP('.',OP('.',THIS,@fname),STR('$init')), [val,THIS,@vslot,LIT(@name.c(as: 'value'))] ) out ||= OP('=',OP('.',THIS,name),val) out = out.c + ';\n' if watchBody @descriptor ||= STACK.root.declare("{oid}$Prop",util.watcher(storageSymbol,watcherSymbol), type: 'const', system: yes) out = "Object.defineProperty(this,{key.c},{@descriptor.c});\n{out}" return out def getter @getter ||= if true if wrapper LIT("()\{ return this.__{name.c}.$get(this,{name.toStr.c},{osym}) \}") else parseTemplate('(){ return $get$; }') def setterForValue value OP('=',OP('.',THIS,storageKey),value) def parseTemplate tpl tpl = tpl.replace(/\$(\w+)\$/g) do |m,key| if key == 'get' GET(THIS,storageSymbol).c elif key == 'name' name.c elif key == 'set' OP('=',GET(THIS,storageSymbol),LIT('value')).c elif key == 'watcher' GET(THIS,watcherSymbol).c else '' LIT(tpl) def setter @setter ||= parseTemplate('(value){ $set$; }') def decorater @decorater ||= if true util.decorate(Arr.new(@decorators),target,name,LIT('null')) export class ClassProperty < ClassField export class ClassAttribute < ClassField def hasConstructorInits !isStatic and value def getter @getter ||= if true let op = CALL(GET(THIS,'getAttribute'),[name.toAttrString]) FN([],[op]) def setter @setter ||= if true let op = CALL(GET(THIS,'setAttribute'),[name.toAttrString,LIT('value')]) FN([LIT('value')],[op]).set(noreturn: true) export class ClassBody < Block def setup super @fields = [] @staticFields = [] def visit stack @scope.visit if @scope if stack and stack.@tag @tag = stack.@tag for node,i in @nodes if node isa Tag # add as render method # console.log 'add as render method' unless node.tagName == 'self' # console.log "cannot use non-self tagname in class" let ast = node.@options:type or node ast.error "only tag allowed here" let meth = MethodDeclaration.new([],[node],Identifier.new('render'),null,{}) @nodes[i] = node = meth node and node.traverse self export class ExpressionList < Block export class VarDeclList < Block def type option(:type) or 'var' def add part push(BR) if @nodes:length let node = VarDeclaration.new(part[0],part[1],type).set(decl: self, datatype: part[0].option(:datatype)) unless @firstDeclaration @firstDeclaration = node node.set(keyword: keyword) push(node) return self def consume node if @nodes:length == 1 return @nodes[0].consume(node) return self # Could inherit from valueNode export class Parens < ValueNode def initialize value, open, close setup @open = open @close = close @value = load(value) def unwrappedNode @value.unwrappedNode def loc try let a = @open.loc let b = @close.loc return [a[0],b[1]] catch e [0,0] def load value @noparen = no value isa Block and value.count == 1 ? value.first : value def isString # checking if this is an interpolated string @open and String(@open) == '("' or value.isString def js o var par = up var v = @value var str = null @noparen = yes if v isa Func if par isa Block # is it worth it? @noparen = yes unless o.isExpression str = v isa Array ? AST.cary(v) : v.c(expression: o.isExpression) else str = v isa Array ? AST.cary(v) : v.c(expression: yes) # check if we really need parens here? if datatype and STACK.tsc str = datatype.c + '(' + str + ')' return str def set obj # console.log "Parens set {JSON.stringify(obj)}" super(obj) def shouldParenthesize # no need to parenthesize if this is a line in a block return no if @noparen # or par isa ArgList return yes def prebreak br super(br) console.log "PREBREAK" @value.prebreak(br) if @value self def isExpressable @value.isExpressable def consume node @value.consume(node) export class PureExpression < Parens # Could inherit from valueNode # an explicit expression-block (with parens) is somewhat different # can be used to return after an expression export class ExpressionBlock < ListNode def c o map(|item| item.c(o) ).join(",") def consume node value.consume(node) # STATEMENTS export class Return < Statement prop value def initialize v @traversed = no @value = v isa ArgList and v.count == 1 ? v.last : v return self def visit if @value isa VarReference @value.option('virtualize',yes) @value.traverse if @value && @value:traverse def startLoc let l = (keyword or @value) l ? l.startLoc : null def js o var v = @value let k = M('return',keyword) if v isa ArgList return "{k} [{v.c(expression: yes)}]" elif v return "{k} {v.c(expression: yes)}" else k def c if STACK.tsc and value isa Self return "{M('return',keyword)} {M('this',value)}" return super if !value or value.isExpressable value.consume(self).c def consume node return self export class ImplicitReturn < Return export class GreedyReturn < ImplicitReturn # cannot live inside an expression(!) export class Throw < Statement def js o "throw {value.c}" def consume node # ROADMAP should possibly consume to the value of throw and then throw? return self export class LoopFlowStatement < Statement prop literal prop expression def initialize lit, expr self.literal = lit self.expression = expr def visit expression.traverse if expression def consume node self def c return super unless expression # get up to the outer loop var _loop = STACK.up(Loop) # need to fix the grammar for this. Right now it # is like a fake call, but should only care about the first argument var expr = self.expression if _loop.catcher expr = expr.consume(_loop.catcher) var copy = self:constructor.new(literal) Block.new([expr,copy]).c elif expr var copy = self:constructor.new(literal) Block.new([expr,copy]).c else super # return "loopflow" export class BreakStatement < LoopFlowStatement def js o do "break" export class ContinueStatement < LoopFlowStatement def js o do "continue" export class DebuggerStatement < Statement def consume node return self # PARAMS export class Param < Node prop name prop index prop defaults prop splat prop variable prop value def initialize value, defaults, typ # could have introduced bugs by moving back to identifier here if typeof value == 'string' value = Identifier.new(value) @traversed = no @name = value @value = value @defaults = defaults @typ = typ @variable = null def varname @variable ? @variable.c : name def datatype super or @value.datatype def type 'param' def jsdoc let typ = datatype if typ && name typ.asParam(name) # '@param {' + typ.c() + '} ' + name else '' def js stack, params if !params or params:as != 'declaration' return "{@value.c}" # include type?? if @defaults return "{@value.c} = {@defaults.c}" elif option(:splat) return "..." + @value.c else return @value.c def visit stack @defaults.traverse if @defaults @value.traverse(declaring: 'param') if @value # self.variable ||= scope__.register(name,self) if @value isa Identifier @value.@variable ||= scope__.register(@value.symbol,@value,type: type) self def assignment OP('=',variable.accessor,defaults) def isExpressable !defaults || defaults.isExpressable def dump {loc: loc} def loc @name && @name.region def toJSON { type: typeName name: name defaults: defaults } export class RestParam < Param export class BlockParam < Param def c "blockparam" def loc # hacky.. cannot know for sure that this is right? var r = name.region [r[0] - 1,r[1]] export class OptionalParam < Param export class NamedParam < Param export class RequiredParam < Param export class ParamList < ListNode prop splat prop block def at index, force = no, name = null, value = null if force while index >= count let curr = count == index let val = curr ? value : null add(Param.new(curr && name || "_{count}",val)) # is this ever traversed? # need to visit at the same time, no? list[index] def metadata filter(|par| !(par isa Meta)) def toJSON metadata def jsdoc let out = [] for item in nodes when item isa Param if item.datatype # option(:datatype) out.push(item.jsdoc()) let doc = out.join('\n') return (doc ? '/**\n' + doc + '\n*/\n' : '') def visit var blk = filter(|par| par isa BlockParam) if blk:length > 1 blk[1].warn "a method can only have one &block parameter" elif blk[0] && blk[0] != last blk[0].warn "&block must be the last parameter of a method" # warn "&block must be the last parameter of a method", blk[0] # add more warnings later(!) # should probably throw error as well to stop compilation # need to register the required-pars as variables super def js stack return EMPTY if count == 0 # FIXME This can be removed for v2? if stack.parent isa Block return head(stack) # items = map(|arg| arg.name.c ).compact # return null unless items[0] if stack.parent isa Code # return "params_here" # remove the splat, for sure.. need to handle the other items as well # this is messy with references to argvars etc etc. Fix let inline = !(stack.parent isa MethodDeclaration) var pars = nodes var opts = {as: 'declaration', typed: inline} # pars = filter(|arg| arg != @splat && !(arg isa BlockParam)) if @splat # pars = filter(|arg| arg isa RequiredParam or arg isa OptionalParam) if @splat AST.compact(nodes.map(|param| let part = param.c(opts) let typ = inline and param.datatype if typ part = typ.c + part return part )).join(",") else throw "not implemented paramlist js" "ta" + AST.compact(map(|arg| arg.c )).join(",") def head o var reg = [] var opt = [] var blk = null var splat = null var named = null var arys = [] var signature = [] var idx = 0 nodes.forEach do |par,i| if par isa RawScript return par.index = idx if par isa OptionalParam signature.push('opt') opt.push(par) elif par isa BlockParam signature.push('blk') blk = par else signature.push('reg') reg.push(par) idx++ if named var namedvar = named.variable # var opt = nodes.filter(|n| n isa OptionalParam) # var blk = nodes.filter(|n| n isa BlockParam)[0] # var splat = nodes.filter(|n| n isa SplatParam)[0] # simple situation where we simply switch # can probably optimize by not looking at arguments at all var ast = [] var isFunc = do |js| "typeof {js} == 'function'" # This is broken when dealing with iframes anc XSS scripting # but for now it is the best test for actual arguments # can also do constructor.name == 'Object' var isObj = do |js| "{js}.constructor === Object" var isntObj = do |js| "{js}.constructor !== Object" # should handle some common cases in a cleaner (less verbose) manner # does this work with default params after optional ones? Is that even worth anything? # this only works in one direction now, unlike TupleAssign # we dont really check the length etc now -- so it is buggy for lots of arguments # if we have optional params in the regular order etc we can go the easy route # slightly hacky now. Should refactor all of these to use the signature? if !named && !splat && !blk && opt:length > 0 && signature.join(" ").match(/opt$/) for par,i in opt ast.push "if({par.name.c} === undefined) {par.name.c} = {par.defaults.c}" elif named && !splat && !blk && opt:length == 0 # and no block?! # different shorthands # if named ast.push "if(!{namedvar.c}||{isntObj(namedvar.c)}) {namedvar.c} = \{\}" elif blk && opt:length == 1 && !splat && !named var op = opt[0] var opn = op.name.c var bn = blk.name.c ast.push "if({bn}==undefined && {isFunc(opn)}) {bn} = {opn},{opn} = {op.defaults.c}" ast.push "if({opn}==undefined) {opn} = {op.defaults.c}" elif blk && named && opt:length == 0 && !splat var bn = blk.name.c ast.push "if({bn}==undefined && {isFunc(namedvar.c)}) {bn} = {namedvar.c},{namedvar.c} = \{\}" ast.push "else if(!{namedvar.c}||{isntObj(namedvar.c)}) {namedvar.c} = \{\}" elif opt:length > 0 || splat # && blk # && !splat var argvar = scope__.temporary(self, pool: 'arguments').predeclared.c var len = scope__.temporary(self, pool: 'counter').predeclared.c var last = "{argvar}[{len}-1]" var pop = "{argvar}[--{len}]" ast.push "var {argvar} = arguments, {len} = {argvar}.length" if blk var bn = blk.name.c if splat ast.push "var {bn} = {isFunc(last)} ? {pop} : null" elif reg:length > 0 # ast.push "// several regs really?" ast.push "var {bn} = {len} > {reg:length} && {isFunc(last)} ? {pop} : null" else ast.push "var {bn} = {isFunc(last)} ? {pop} : null" # if we have named params - look for them before splat # should probably loop through pars in the same order they were added # should it be prioritized above optional objects?? if named # should not include it when there is a splat? ast.push "var {namedvar.c} = {last}&&{isObj(last)} ? {pop} : \{\}" for par,i in opt ast.push "if({len} < {par.index + 1}) {par.name.c} = {par.defaults.c}" # add the splat if splat var sn = splat.name.c var si = splat.index if si == 0 ast.push "var {sn} = new Array({len}>{si} ? {len} : 0)" ast.push "while({len}>{si}) {sn}[{len}-1] = {pop}" else ast.push "var {sn} = new Array({len}>{si} ? {len}-{si} : 0)" ast.push "while({len}>{si}) {sn}[--{len} - {si}] = {argvar}[{len}]" # if named # for k,i in named.nodes # # OP('.',namedvar) <- this is the right way, with invalid names etc # var op = OP('.',namedvar,k.key).c # ast.push "var {k.key.c} = {op} !== undefined ? {op} : {k.value.c}" # if named # return ast.join(";\n") + ";" # return "if({opt[0].name.c} instanceof Function) {blk.c} = {opt[0].c};" elif opt:length > 0 for par,i in opt ast.push "if({par.name.c} === undefined) {par.name.c} = {par.defaults.c}" # now set stuff if named params(!) if named for k,i in named.nodes # console.log "named var {k.c}" var op = OP('.',namedvar,k.c).c ast.push "var {k.c} = {op} !== undefined ? {op} : {k.defaults.c}" if arys:length for v,i in arys # create tuples v.head(o,ast,self) # ast.push v.c # if opt:length == 0 return ast:length > 0 ? (ast.join(";\n") + ";") : EMPTY # Legacy. Should move away from this? export class ScopeVariables < ListNode # for later, moz-ast style prop kind prop split # we want to register these variables in def add name, init, pos = -1 var vardec = VariableDeclarator.new(name,init) vardec.variable = name if name isa Variable pos == 0 ? unshift(vardec) : push(vardec) vardec def load list list.map do |par| VariableDeclarator.new(par.name,par.defaults,par.splat) def isExpressable nodes.every(|item| item.isExpressable) def js o return EMPTY if count == 0 # When is this needed? if count == 1 && !isExpressable first.variable.autodeclare return first.assignment.c var keyword = 'var' var groups = {} nodes.forEach do |item| let variable = item.@variable or item let typ = variable isa Variable and variable.type if typ groups[typ] ||= [] groups[typ].push(item) # elif item.@variable # console.log 'variable without type' # else # console.log "no variable type?? {item:constructor} {item.type}" if groups['let'] and (groups['var'] or groups['const']) groups['let'].forEach do |item| (item.@variable or item).@virtual = yes elif groups['let'] keyword = 'let' # FIX PERFORMANCE # This is used in let scope as well - inflexible # Is this used by if split and true let out2 = [] for own k,v of groups out2.push "{k} {AST.cary(v,as: 'declaration').join(', ')};" return out2.join("\n") var out = AST.compact(AST.cary(nodes,as: 'declaration')).join(", ") out ? "{keyword} {out}" : "" export class VariableDeclarator < Param prop type # can possibly create the variable immediately but wait with scope-declaring # What if this is merely the declaration of a system/temporary variable? def visit # even if we should traverse the defaults as if this variable does not exist # we need to preregister it and then activate it later self.variable ||= scope__.register(name,null, type: @type or 'var') defaults.traverse if defaults # WARN what if it is already declared? self.variable.declarator = self self.variable.addReference(name) self # needs to be linked up to the actual scoped variables, no? def js o return null if variable.@proxy var defs = defaults let typ = variable.datatype # FIXME need to deal with var-defines within other statements etc # FIXME need better syntax for this if defs != null && defs != undefined defs = defs.c(expression: yes) if defs isa Node defs = "{typ.c}({defs})" if typ "{variable.c} = {defs}" elif typ "{variable.c} = {typ.c}(undefined)" else "{variable.c}" def accessor self export class VarDeclaration < Node prop kind prop left prop right def op @op def type @kind def initialize left, right, kind, op = '=' @op = op @left = left @right = right @kind = kind def visit stack # WARN not always correct unless @left isa Identifier and @right isa Func @right.traverse if @right @variables = scope__.captureVariableDeclarations do @left.traverse(declaring: type) if @left # replace directly # should the left side be wrapped in a VarDeclLeft node? if @left isa Identifier # TODO add an identifier.declare method for this @left.@variable ||= scope__.register(@left.symbol,@left,type: type) @right.traverse if @right # elif @left isa Obj # # need to traverse the object to register variables # # if it is a let we might also need to rename them though self def isExpressable no def consume node if node isa TagLike return self if node isa PushAssign or node isa Return let ast = self if right and !right.isExpressable let temp = scope__.temporary(self) let ast = right.consume(OP('=',temp,NULL)) right = temp return Block.new([ast,BR,self.consume(node)]) return Block.new([ast,BR,@left.consume(node)]) if node isa Return return Block.new([self,BR,@left.consume(node)]) super(node) def c o if right and !right.isExpressable let temp = scope__.temporary(self) let ast = right.consume(OP('=',temp,NULL)) right = temp return Block.new([ast,BR,self]).c(o) # testing this return super(o) def js let out = '' let kind = kind let typ = (datatype or (@left and @left.datatype)) if STACK.tsc and @variables:length > 1 and @variables.some(do $1.vartype) kind = 'let' # or var? for item in @variables if item.vartype out += item.vartype.c + ' ' out += "{M(kind,keyword)} {item.c()};\n" out += "({left.c()}" if right out += " = {right.c(expression: true)}" out += ")" else out += "{M(kind,keyword)} {left.c()}" if right out += " = {right.c(expression: true)}" if option(:export) out = M('export',option(:export)) + " {out}" if typ out = typ.c() + '\n' + out return out # TODO clean up and refactor all the different representations of vars export class VarName < ValueNode prop variable prop splat def initialize a,b super @splat = b def visit # should we not lookup instead? # FIXME p "register value {value.c}" self.variable ||= scope__.register(value.c,null) self.variable.declarator = self self.variable.addReference(value) self def js o variable.c def c variable.c # CODE export class Code < Node prop head prop body prop scope prop params def isStatementLike yes def scopetype Scope def visit @scope.visit if @scope # @scope.parent = STACK.scope(1) if @scope self export class CodeBlock < Code def initialize body, opts @traversed = no @body = AST.blk(body) @scope = FlowScope.new(self) @body.head = @scope.head @options = {} def visit @scope.visit @body.traverse self def c # add curly braces? @body.c # Rename to Program? export class Root < Code def initialize body, opts @traversed = no @body = AST.blk(body) @scope = RootScope.new(self,null) @options = {} def loc @body.loc def visit ROOT = STACK.ROOT = @scope try scope.visit body.traverse if body.first isa Terminator body.first.@first = yes catch e let err = ImbaTraverseError.wrap(e) err.@sourcePath = OPTS:sourcePath err.@loc = STACK.currentRegion() throw err def compile o, script = {} STACK.reset # -- nested compilation does not work now @scope.options = OPTS = STACK.@options = @options = o or {} STACK.root = @scope @scope.@imba.configure(o) traverse STACK.root = @scope if o:bundle if o:cwd and STACK.isNode let abs = fspath.resolve(o:cwd,o:sourcePath) let rel = fspath.relative(o:cwd,abs).split(fspath:sep).join('/') let np = @scope.importProxy('path').proxy # TODO Test this thoroughly - better to replace fater the fact? @scope.lookup('__filename'):c = do LIT("{np:resolve}({STR(rel).c})").c @scope.lookup('__dirname'):c = do LIT("{np:dirname}({np:resolve}({STR(rel).c}))").c else @scope.lookup('__filename').@c = STR(o:sourcePath).c @scope.lookup('__dirname').@c = STR(fspath.dirname(o:sourcePath)).c if o:onTraversed isa Function o:onTraversed(self,STACK) let sheet = STACK.css let css = sheet.toString if sheet:transitions runtime:transitions if css and (!o:styles or o:styles == 'inline') runtime:styles var out = c(o) if STACK.tsc # out = "import 'imba/index.d.ts';\n{out}" out = "export \{\};String();\n{out}\n" if script:sourceCode and script:sourceCode.match(/(^|[\r\n])\# @nocheck[\n\r]/) out = "// @ts-nocheck\n{out}" script:rawResult = { js: out css: css } script:js = out script:css = css or "" script:sourceId = sourceId script:assets = scope.assets script:universal = STACK.meta:universal !== no if !STACK.tsc if script:css and (!o:styles or o:styles == 'inline') # let style = '`\n' + script:css + '\n`' # - we have to escape it - right? let style = JSON.stringify(script:css) script:js = "{script:js}\n{runtime:styles}.register('{script:sourceId}',{style});" if o:debug or true script:js += '\n/*\n' + script:css + '\n*/\n' # this we should only include when debugging # always create the sourcemap? if o:sourcemap or STACK.tsc # should handle sourcemap no matter what? let map = SourceMap.new(script,o).generate script:sourcemap = map.result if o:sourcemap == 'inline' script:js += map.inlined unless o:raw script:css &&= SourceMapper.strip(script:css) script:js = SourceMapper.strip(script:js) if STACK.tsc # now combine comments - keep whitespace to not mess up sourcemapping script:js = script:js.replace(/\*\/\s[\r\n]+(\t*)\/\*\*/gm) do |m| m.replace(/[^\n\t]/g,' ') return script def js o var out = scope.c # find and replace shebangs var shebangs = [] out = out.replace(/^[ \t]*\/\/(\!.+)$/mg) do |m,shebang| shebang = shebang.replace(/\bimba\b/g,'node') shebangs.push("#{shebang}\n") return "" out = shebangs.join('') + out return out def analyze o = {} # loglevel: 0, entities: no, scopes: yes STACK.loglevel = o:loglevel or 0 STACK.@analyzing = true ROOT = STACK.ROOT = @scope OPTS = STACK.@options = { platform: o:platform loglevel: o:loglevel or 0 analysis: { entities: (o:entities or no), scopes: (o:scopes ?= yes) } } traverse STACK.@analyzing = false return scope.dump def inspect true export class ClassDeclaration < Code prop name prop superclass prop initor def consume node if node isa Return option('return',node) return self super def namepath @namepath ||= "{name ? name.c : '--'}" def metadata { type: 'class' namepath: namepath inherits: superclass?.namepath path: name and name.c.toString desc: @desc loc: loc symbols: @scope.entities } def loc if let d = option(:keyword) [d.@loc,body.loc[1]] else super def startLoc @startLoc ?= MSTART(option(:export),option(:keyword)) def endLoc @endLoc ?= MEND(body) def toJSON metadata def isStruct keyword and String(keyword) == 'struct' def isExtension option(:extension) def isGlobal option(:global) def isNamespaced @name isa Access def exportForDts return false unless STACK.tsc return true if isNamespaced and (!@name.left.@variable or @name.left.@variable.isImported) return false if isNamespaced and (@name.left.@variable and !@name.left.@variable.isImported) return true if isGlobal and !option(:export) return true if isExtension and (!@name.@variable or @name.@variable.isImported) return false # STACK.tsc and !!((isNamespaced and (!@name.left.@variable or @name.left.@variable.isImported)) or (isGlobal or isExtension) def initialize name, superclass, body # what about the namespace? @traversed = no if name isa VarOrAccess name = name.@value @name = name # or LIT('') @superclass = superclass @scope = isTag ? TagScope.new(self) : ClassScope.new(self) @body = AST.blk(body) or ClassBody.new([]) @entities = {} # items should register the entities as they come self def isTag no def staticInit # @superclass ? 'super.inherited instanceof Function && super.inherited(this)' : 'this' @staticInit ||= addMethod(initKey,[],'this').set(static: yes) # add static block def initKey @initKey ||= (STACK.tsc ? STACK.root.symbolRef('#__init__') : SymbolIdentifier.new('#__init__')) def patchKey @patchKey ||= (STACK.tsc ? STACK.root.symbolRef('#__patch__') : SymbolIdentifier.new('#__patch__')) def initPath @initPath ||= OP('.',LIT('super'),initKey) def virtualSuper @virtualSuper ||= @scope.parent.declare(:tmp,null,system: yes, type: 'let') def classReference @name def instanceInit return @instanceInit if @instanceInit let call = Super.callOp(initKey) if @superclass call = OP('&&',LIT('deep'),OP('&&',OP('.',LIT('super'),initKey),call)) let fn = addMethod(initKey,[],(isTag or @superclass) ? [call,BR] : '',{}) do |fun| yes fn.set(noreturn: yes) fn.params.at(0,yes,'$$',LIT('null')) # (restIndex,yes,'$$',LIT('{}')) fn.params.at(1,yes,'deep',LIT('true')) @instanceInit = fn def instancePatch return @instancePatch if @instancePatch let body = [] let fn = addMethod(patchKey,[],body,{}) do |fun| yes let param = fn.@params.at(0,yes,'$$',LIT('{}')) if @superclass let call = Super.callOp(patchKey,[param]) call = OP('&&',OP('.',LIT('super'),patchKey),call) fn.inject(call) fn.set(noreturn: yes) @instancePatch = fn def isInitingFields @inits or (@supernode and @supernode:isInitingFields and @supernode.isInitingFields) def visit # replace with some advanced lookup? @body.@delimiter = '' let blk = STACK.up(Block) @decorators = blk and blk.collectDecorators # console.log "FOUND DECORATORS FOR CLASS?",@decorators and @decorators:length STACK.pop(self) let sup = @superclass @path = @name @ownName = @name @realName = @name isa Access ? @name.right : @name # let dtsify = STACK.tsc and (isGlobal or isExtension or (isNamespaced and ) if sup sup.traverse() # also visit and possibly declare the class name? if sup isa VarOrAccess if sup.@variable let val = sup.@variable.value if val isa ClassDeclaration @supernode = val elif sup.symbol == 'Object' if !STACK.tsc sup = @superclass = null else @autosuper = yes try sup.@identifier.@symbol = 'ΤObject' # sup.@value = 'AnyObject' if isExtension and @name @name.traverse() if @name isa Identifier @name.resolveVariable() if !isTag let extname @className = @name @ownName = STACK.toInternalClassName(@name) @mixinName = scope__.register(@ownName,null) else @className = LIT(@name.toClassName) @ownName = STACK.toInternalClassName(@name) @mixinName = scope__.register(@ownName,null) elif @name isa Identifier if !isTag or @name.isCapitalized @name.registerVariable('const') @name.@variable.value = self elif @name and !(@name isa Access) @name.traverse(declaring: self) elif @name @name.traverse() if isGlobal and !isExtension and !isNamespaced and option(:export) and STACK.tsc @exportName = STACK.toInternalClassName(@name) if @ownName == @name and (exportForDts) if isGlobal and !isExtension and !isNamespaced and option(:export) @exportName = STACK.toInternalClassName(@name) else @ownName = STACK.toInternalClassName(@name) # console.log 'visited class',exportForDts,String(@ownName),String(@exportName) STACK.push(self) ROOT.entities.add(namepath,self) scope.visit set(iife: STACK.up isa Instantiation) var separateInitChain = true var fields = [] var signature = [] var params = [] var declaredFields = {} var restIndex = undefined var instanceMethodMap = {} for node in body if node isa ClassField if !node.isStatic let name = String(node.name) declaredFields[name] = node if separateInitChain node.set(restIndex: 0) # if node.watchBody # console.log 'has watcher??' if node isa MethodDeclaration let name = node.rawName if node.isMember instanceMethodMap[name] = node # TODO No longer used - remove if option(:params) # find the rest param let add = [] for param,index in option(:params) if param isa RestParam restIndex = index continue let name = String(param.name) let field = declaredFields[name] let dtyp = param.option(:datatype) if !field field = fields[name] = ClassField.new(param.name).set( datatype: dtyp value: param.defaults ) # field.set(param: param) add.push(field) params.push(param) else if dtyp and !field.datatype field.set(datatype: dtyp) if param.defaults and !field.value field.set(value: param.defaults) if field field.set(paramIndex: index, paramName: name) for item in add.reverse body.unshift(item) body.traverse var ctor = body.option(:ctor) # if STACK.tsc # if ctor and @autosuper # ctor.body.add([LIT("super()"),BR],0) # # return self let tsc = STACK.tsc var inits = InstanceInitBlock.new() var staticInits = @staticInits = ClassInitBlock.new() var patches = InstancePatchBlock.new() if @realName staticInits.add util.defineName(THIS,@realName.toStr),0 # OP('=',LIT('this[Symbol.for("#name")]'),@realName.toStr),0 var ctor = body.option(:ctor) let fieldNodes = body.filter do |node| node isa ClassField let allDecorators = [] for node in fieldNodes if node.watchBody addMethod(node.watcherSymbol,[],[node.watchBody],{}) do |fn| node.@watchMethod = fn node.@watchParam = fn.params.at(0,yes,'e') if node.hasStaticInits and !node.option(:declareOnly) staticInits.add(node) if node.hasConstructorInits if isExtension if node.value node.@name.warn "field with value not supported in class extension" elif !node.option(:declareOnly) inits.add(node) patches.add(node) if !node.isStatic and restIndex != null node.set(restIndex: restIndex) if !tsc and @decorators let op = util.decorate(Arr.new(@decorators),THIS) staticInits.add([op,BR]) allDecorators.push(@decorators) for node,i in body if node.@decorators let target = node.option(:static) ? THIS : PROTO let desc = LIT('null') let op = util.decorate(Arr.new(node.@decorators),target,node.name,desc) allDecorators.push(node.@decorators) staticInits.add([op,BR]) if !inits.isEmpty and !tsc @inits = inits instanceInit inits.set(ctor: instanceInit) instanceInit.inject(inits) if isTag # tags always call init from outside the actual construction yes elif !@superclass let initop = OP('.',THIS,initKey) if !ctor ctor = addMethod('constructor',[],[],{}) let param = ctor.params.at(0,yes,'$$',LIT('null')) let callop = CALL(initop,[param]) unless tsc ctor.body.add([callop,BR],0) else let sup = ctor.option(:supr) if sup sup:real.set(target: initop,args: []) else ctor.body.add([CALL(initop,[]),BR],0) elif !@supernode or !@supernode.isInitingFields # we don't have an explicit constructor # if we cannot know on compiletime that the superclass # has an initor - we do need to call it here let op = OP('||',initPath,CALL(OP('.',THIS,initKey),[])) if !ctor ctor = addMethod('constructor',[],[Super.new(),BR,op],{}) else let after = ctor.option(:injectInitAfter) ctor.inject(op,after ? {after: after} : 0) yes if !patches.isEmpty and !tsc instancePatch patches.set(ctor: instancePatch) instancePatch.inject(patches) if tsc and ctor and @autosuper ctor.body.add([LIT("super()"),BR],0) # staticInit.inject(staticInits,0) if !STACK.tsc let hasInitedHook = !!instanceMethodMap["#__inited__"] let hasDecorators = allDecorators:length > 0 if hasDecorators STACK.use('hooks') let decosym = STACK.symbolFor('#__hooks__') staticInits.unshift LIT("this.prototype[{decosym}] = {runtime:hooks}"), yes # staticInit.inject LIT("this.prototype[{decosym}] = {runtime:hooks}"), 0 if !isTag and !ctor and (hasInitedHook or hasDecorators) let ops = sup ? [Super.new(),BR] : [BR] ctor = addMethod('constructor',[],ops,{}) if ctor and !isTag and !STACK.isStdLib let ctorsym = STACK.symbolFor('#__initor__') let initsym = STACK.symbolFor('#__inited__') let hooksym = STACK.symbolFor('#__hooks__') let initedHook = LIT("this[{hooksym}]&&this[{hooksym}].inited(this)") # console.log "emitInited",initedHook,hasDecorators,hasInitedHook,STACK.imbaPath if sup let refsym = STACK.getSymbol() staticInits.unshift LIT("this.prototype[{ctorsym}] = {refsym}"), 0 if hasInitedHook ctor.inject LIT("if(this[{ctorsym}]==={refsym}) ({initedHook},this[{initsym}]());") else ctor.inject LIT("this[{ctorsym}]==={refsym} && ({initedHook},this[{initsym}] && this[{initsym}]())") else # if hasDecorators # staticInit.inject LIT("this.prototype[{decosym}] = true"), 0 if hasInitedHook ctor.inject LIT("if(!this[{ctorsym}]) ({initedHook},this[{initsym}]());") elif hasDecorators ctor.inject LIT("!this[{ctorsym}] && this[{hooksym}].inited(this);") # If we are not in the stdlib / core - add an inherited hook for the class? # ctor.body.add([callop,BR],0) if !tsc and sup # add a reference to the namespace as well? staticInits.add util.inheritClass(THIS) # staticInits.add LIT('Object.getPrototypeOf(this.prototype).constructor?.inherited?.(this)') if !staticInits.isEmpty and !tsc body.add([BR,staticInits]) self def addMethod name, params, mbody, options, cb mbody = [LIT(mbody)] if mbody isa String name = Identifier.new(name) if name isa String let func = MethodDeclaration.new(params,mbody or [],name,null,options or {}) self.body.unshift(func,yes) if cb isa Function cb(func) func.traverse return func # def addGetter name, body # def addSetter name, body def js o scope.virtualize # is this always needed? scope.context.value = name scope.context.reference = name var up = STACK.up var o = @options or {} var cname = @ownName isa Access ? @ownName.right : @ownName var origName = @name isa Access ? @name.right : @name # var namespaced = name != cname var initor = null var sup = superclass if typeof cname != 'string' and cname cname = cname.c(mark: yes) @cname = cname # if origName # staticInits.add LIT('this[Symbol.for("#name")] = "' + origName.c + '"'),0 var externalAccess = LIT(cname) var supAccess = null if STACK.tsc and isExtension and !exportForDts # (isNamespaced or @name.@variable and !@name.@variable.isImported) let parts = [] let target = @name.c for part in body.@nodes parts.push(part.c(as: 'descriptor',target: target)) return parts.join(';\n') let jsbody = body.c() let jshead = M('class',keyword) # "{C(keyword,mark: yes)}" if name jshead += " {M cname, name}" else # console.log 'are we in an assignment?',"{STACK.up}" if up isa VarReference try jshead += " {up.@value.@symbol}" if sup supAccess = M(sup) jshead += " extends {supAccess}" if name isa Access and !exportForDts and !isExtension # TODO or the definition is local jshead = "{name.c} = {jshead}" if option(:export) or (STACK.tsc and exportForDts) # beware of double export if option(:default) jshead = "{M 'export',option(:export)} {M 'default',option(:default)} {jshead}" else jshead = "{M 'export',option(:export)} {jshead}" let js = "{jshead} \{{jsbody}\}" if isExtension and !STACK.tsc let extTarget = self isa ExtendDeclaration ? LIT(@className.c) : LIT("{@className.c}.prototype") if @virtualSuper let wrap = OP('=',@virtualSuper,util.virtualSuper(extTarget)) extTarget = LIT("({wrap.c},{extTarget.c})") # TODO can this happen in static block? js += ";\n" + util.extend(extTarget,LIT("{externalAccess}.prototype")).c + ';\n' if option(:global) and !STACK.tsc # external access? # js = "{js}; {scope__.root.globalRef}.{@cname} = {@cname}" let access = name isa Access let getter = name isa Access ? name.c : @cname if STACK.tsc js = "{js};{!access ? " export \{{@cname}\};" : ""} {scope__.root.globalRef}.{@cname} = {getter}" else js = "{js}; {scope__.root.globalRef}.{@cname} = {getter}" # if option(:export) and option(:global) and STACK.tsc if STACK.tsc and @exportName js = "{js}; export \{{@ownName} as {@exportName}\}" return js export class ExtendDeclaration < ClassDeclaration export class TagDeclaration < ClassDeclaration def isTag yes def isInitingFields yes def namepath "<{name}>" def metadata Object.assign(super,{ type: 'tag' }) def cssns @cssns ||= @scope.cssns def cssid @cssid ||= @scope.cssid def classReference LIT(@name.toClassName) def cssref scope if isNeverExtended and !superclass return @cssns if scope let s = scope.closure return s.memovar('_ns_',OP('||',OP('.',s.context,'_ns_'),STR(''))) else OP('||',OP('.',THIS,'_ns_'),STR('')) def isNeverExtended if name and name.isClass return !option(:export) and !option(:extended) else no def visit if STACK.hmr self.cssid self.cssns super let sup = superclass @config = {} if sup and !STACK.tsc if (sup.isNative or sup.isNativeSVG) let op = sup.nativeCreateNode op = util.extendTag(op,THIS) addMethod('create$',[],[op]).set(static: yes) set(extends: Obj.wrap(extends: sup.name)) @config:extends = sup.name elif sup.isClass sup.resolveVariable(scope__.parent) let up = sup.@variable && sup.@variable.value up.set(extended: self) if up if @elementReferences for own ref,child of @elementReferences if STACK.tsc let val = child.option(:reference) let typ = child.type let op = "{M(AST.sym(val),val)}" if typ and typ:toClassName op += " = new {typ.toClassName}" self.body.unshift(LIT(op + ';'),yes) # add some default css if !STACK.tsc and name and name:toNodeName and !option(:extension) # name.@nodeName ||= STACK.sourceId + '-' + STACK.generateId('') let name = name.toNodeName name = name + '-tag' if name.indexOf('-') == -1 STACK.css.add name + ' { display:block; }' if option(:export) and name and name:isLowerCase and name.isLowerCase warn "Lowercased tags are globally available - not exportable", loc: option(:export) return def addElementReference name, child let refs = @elementReferences ||= {} if refs[name] and refs[name] != child child.warn("Duplicate elements with same reference", loc: name) else refs[name] = child child.set(tagdeclbody: @body) return child def js s scope.virtualize # is this always needed? scope.context.value = name scope.context.reference = name let className = name.toClassName let sup = superclass let anonGlobalTag = !option(:extension) and (!name.isClass) and STACK.tsc if sup and sup.@variable sup = sup.@variable elif sup sup = CALL(runtime:getSuperTagType,[sup,STR(sup.toClassName),runtime:Component]) else sup = runtime:Component if STACK.tsc sup = superclass ? superclass.toClassName : LIT('imba.Component') if option(:extension) # className = "Extended${className}" # ought to support the nested stuff? # let out = "class Ω{name.toExtensionName.replace(/\./g,'__')}Ω{STACK.incr('extend')}" let out = "class {@mixinName.c}" # extends {@className.c} let obody = body.c if STACK.tsc and false # if the class-name is aliased via an import we want to include that here # console.log 'cl',@className obody = "\n__extends__= new {M(@className.c,@name)};\n{obody}" out = "{out} \{{obody}\}" out = "export {out}" unless @name.@variable return out else body.unshift(LIT('static $$TAG$$\n')) elif option(:extension) let namevar = @name.@variable let cls = namevar or CALL(runtime:getTagType,[name,STR(name.toClassName)]) if className == 'ImbaElement' or className == 'imba.Component' cls = runtime:Component let tagname = TagTypeIdentifier.new(name) let js = "(class \{{body.c}\}).prototype" return util.extend("{cls.c}.prototype",LIT(js)).c else if name.isNative name.error "tag {name.symbol} already exists" let closure = scope__.parent let jsbody = body.c() let jshead = "{M 'class', keyword} {M className, name} extends {M sup, superclass}" if option(:export) if option(:default) jshead = "{M 'export',option(:export)} {M 'default',option(:default)} {jshead}" else jshead = "{M 'export',option(:export)} {jshead}" if anonGlobalTag and STACK.tsc # only tsc # jshead = "globalThis[{M("'{M(name.toNodeName,name)} tag'",name)}] = {M 'class', keyword} extends {M sup, superclass}" # jshead = "globalThis.{M className, name} = {M 'class', keyword} extends {M sup, superclass}" jshead = "export {jshead}" # jshead = "globalThis.{className} = {M 'class', keyword} {M className, name} extends {M sup, superclass}" let js = "{jshead} \{{jsbody}\}" @config:cssns = cssns if @cssns @config:cssid = cssid if @cssid if !STACK.tsc if @staticInit js += "; {OP('.',LIT(className),initKey).c}()" # let ext = (option(:extends) or LIT('{}')).c let ext = Obj.wrap(@config).c if name.isClass @config:name = name.symbol js += "; {runtime:defineTag}({name.c},{className},{ext})" # else # if !option(:extension) and (!name.isClass) # js += "; globalThis.{M name.toTscName, name} = {className};" if STACK.tsc and (option(:global) or !name.isClass) and false js += "; globalThis.{className} = {className}" return js export class Func < Code prop name prop params prop target prop options prop type prop context def scopetype do FunctionScope def initialize params, body, name, target, o @options = o var typ = scopetype @traversed = no @body = AST.blk(body) @scope ||= (o and o:scope) || typ.new(self) @scope.params = @params = ParamList.new(params) @name = name || '' @target = target @type = :function @variable = null self def inject line, o @body.add([line,BR],o) def nonlocals @scope.@nonlocals def returnType datatype def visit stack, o # any function inside descriptors should compile as strong scopes if stack.@descriptor and !stack.tsc @scope = MethodScope.new(self) # (o and o:scope) || typ.new(self) @scope.params = @params # = ParamList.new([]) scope.visit @context = scope.parent @params.traverse(declaring: 'arg') @body.traverse # so soon? def funcKeyword let str = "function" str = "async {str}" if option(:async) return str def jsdoc let o = [] # console.log 'found desc?',@desc if @desc @desc.@skip = yes o.push(@desc.toString) for item in @params.nodes when item isa Param if item.datatype o.push(item.jsdoc()) if option(:inExtension) and @target let kls = @context.node let name = @context.node.@className if name and STACK.tsc # console.log 'inext',@context.node.@className # o.push('@this { this & ' + name.c + ' }') # let thistype = "globalThis." + name.c let thistype = name.c if kls.option(:instanceOnly) thistype = "typeof {thistype}" else thistype = "InstanceType" # o.push('@this ' + thistype + '') # FIXME not used? if option(:jsdocthis) o.push('@this ' + option(:jsdocthis) + '') if returnType o.push('@returns { ' + returnType.asRawType + ' }') let doc = o.join('\n') return (doc ? '/**\n' + doc + '\n*/\n' : '') def js s,o body.consume(ImplicitReturn.new) unless option(:noreturn) var ind = body.@indentation # var s = ind and ind.@open body.@indentation = null if ind and ind.isGenerated var code = scope.c(indent: (!ind or !ind.isGenerated), braces: yes) # args = params.map do |par| par.name # head = params.map do |par| par.c # code = [head,body.c(expression: no)].AST.flatten.compact.join("\n").wrap # FIXME creating the function-name this way is prone to create naming-collisions # will need to wrap the value in a FunctionName which takes care of looking up scope # and possibly dealing with it var name = typeof @name == 'string' ? @name : @name.c name = name ? ' ' + name.replace(/\./g,'_') : '' var keyword = o and o:keyword != undefined ? o:keyword : funcKeyword var out = "{M(keyword,option('def') or option('keyword'))}{helpers.toValidIdentifier(name)}({params.c}) " + code # out = "async {out}" if option(:async) out = "({out})()" if option(:eval) return out def shouldParenthesize par = up par isa Call && par.callee == self # if up as a call? Only if we are export class IsolatedFunc < Func prop leaks def scopetype do IsolatedFunctionScope def isStatic yes def isPrimitive yes def visit stack super return if stack.tsc if let leaks = @scope.@leaks @leaks = [] leaks.forEach do |shadow,source| shadow.@proxy = @params.at(@params.count,yes) @leaks.push(source) self export class Lambda < Func def scopetype var k = option(:keyword) (k and k.@value == 'ƒ') ? (MethodScope) : (LambdaScope) export class ClosedFunc < Func def scopetype MethodScope export class TagFragmentFunc < Func def scopetype # caching still needs to be local no matter what? option(:closed) ? (MethodScope) : (LambdaScope) export class MethodDeclaration < Func prop variable prop decorators def scopetype do MethodScope def consume node if node isa Return option('return',yes) return self super def identifier @name def rawName @name isa Identifier ? (@name.toRaw) : "" def metadata { type: "method" name: "" + name namepath: namepath params: @params.metadata desc: @desc scopenr: scope.@nr loc: loc } def loc if let d = option(:def) let end = body.option(:end) or body.loc[1] [d.@loc,end] else [0,0] def isGetter @type == 'get' def isSetter @type == 'set' def isConstructor String(name) == 'constructor' def isMember !option(:static) def toJSON metadata def namepath return @namepath if @namepath var name = String(name.c) var sep = (option('static') ? '.' : '#') if target let ctx = target # console.log "target?? {@target.@parent} {@context.node}" if ctx.namepath == "ValueNode" ctx = @context.node @namepath = ctx.namepath + sep + name else @namepath = '&' + name def visit @type = option(:type) or (option(:def)?.@value or 'def') @decorators = up?.collectDecorators var o = @options scope.visit if option(:inObject) @params.traverse @body.traverse return self var closure = @context = scope.parent.closure if closure isa RootScope and !target and !(@name isa DecoratorIdentifier) scope.@context = closure.context elif closure isa MethodScope and !target and !(@name isa DecoratorIdentifier) scope.@selfless = yes @params.traverse if @name:isPredicate and @name.isPredicate and !isSetter and !isGetter @name.warn "Only getters/setters should end with ?" if target isa Identifier if let variable = scope.lookup(target.toString) target = variable # should be changed to VarOrAccess?! if String(name) == 'initialize' and (closure isa ClassScope) and !(closure isa TagScope) self.type = :constructor if String(name) == 'constructor' or isConstructor up.set(ctor: self) set(noreturn: yes) # instance-method / member if closure isa ClassScope and !target @target = closure.prototype let inExt = closure.node.option('extension') set( prototype: @target, inClassBody: yes, inExtension: inExt ) # if inExt # scope.context.@value = LIT("/** @type {closure.node.@className}*/(this)") closure.annotate(self) if target isa Self @target = closure.context closure.annotate(self) set(static: yes) elif o:variable @variable = scope.parent.register(name, self, type: String(o:variable)) warn "{String(o:variable)} def cannot have a target" if target elif !target @variable = scope.parent.register(name,self,type: 'const') yes if o:export and !(closure isa RootScope) warn("cannot export non-root method", loc: o:export.loc) ROOT.entities.add(namepath,self) @body.traverse if isConstructor and option(:supr) let ref = scope__.context.@reference let supr = option(:supr) let node = supr:node let block = supr:block if ref ref.declarator.@defaults = null let op = OP('=',ref,This.new) block.replace node, [node,op] self def supername type == :constructor ? type : name # FIXME export global etc are NOT valid for methods inside any other scope than # the outermost scope (root) def js stack,co = {} var o = @options # FIXME Do this in the grammar - remnants of old implementation unless type == :constructor or option(:noreturn) or isSetter() if option(:chainable) body.add(ImplicitReturn.new(scope.context)) elif option(:greedy) # haaack body.consume(GreedyReturn.new) else body.consume(ImplicitReturn.new) var code = scope.c(indent: yes, braces: yes) var name = typeof @name == 'string' ? @name : @name.c(as: 'field') var out = "" if (option(:inClassBody) or option(:inObject)) and co:as != 'descriptor' # what if this is async? let prefix = '' if self.isGetter prefix = M('get',option(:keyword)) + ' ' elif self.isSetter prefix = M('set',option(:keyword)) + ' ' # let prefix = self.isGetter() ? 'get ' : (self.isSetter() ? 'set ' : '') prefix = "async {prefix}" if option(:async) prefix = "{M('static',option(:static))} {prefix}" if option(:static) out = "{prefix}{M name, null, as: 'field'}({params.c}){code}" out = jsdoc() + out # out = @params.jsdoc() + out if option(:declareOnly) and !STACK.tsc return '' return out var func = "({params.c})" + code var ctx = context var fname = helpers.toValidIdentifier(AST.sym(self.name)) if target # special case here? if STACK.tsc # let o = option(:static) ? "typeof {target.c}" # : "InstanceType" set(jsdocthis: "typeof {target.c}") # TODO make this work with SymbolIdentifier if fname[0] == '[' fname = fname.slice(1,-1) else fname = "'{fname}'" if isGetter out = "Object.defineProperty({target.c},{fname},\{get: {jsdoc()}{funcKeyword}{func}, configurable: true\})" return out elif isSetter out = "Object.defineProperty({target.c},{fname},\{set: {jsdoc()}{funcKeyword}{func}, configurable: true\})" return out else # if STACK.tsc # let o = "InstanceType" # set(jsdocthis: o) let k = OP('.',target,@name) out = "{k.c} = {funcKeyword} {func}" if o:export out = "exports.{o:default ? 'default' : fname} = {out}" else out = "{M funcKeyword, keyword} {M fname, @name}{func}" if o:export out = "{M('export',o:export)} {o:default ? M('default ',o:default) : ''}{out}" if o:global out = "{out}; {scope__.root.globalRef}.{fname} = {fname};" if option(:return) out = "return {out}" out = jsdoc() + out if option(:declareOnly) and !STACK.tsc return '' return out # Literals should probably not inherit from the same parent # as arrays, tuples, objects would be better off inheriting # from listnode. export class Literal < ValueNode def initialize v @traversed = no @expression = yes @cache = null @raw = null @value = load(v) def isConstant yes def load value value def toString "" + value def hasSideEffects false def shouldParenthesizeInTernary no def startLoc @startLoc or (@value and @value:startLoc && @value.startLoc) def endLoc @endLoc or (@value and @value:endLoc && @value.endLoc) export class RawScript < Literal def c @value export class Bool < Literal # Should keep the real value (yes/no/true/false)? def initialize v @value = v @raw = String(v) == "true" ? true : false def cache self def isPrimitive yes def truthy String(value) == "true" # yes def js o String(@value) def c STACK.@counter += 1 # undefined should not be a bool String(@value) # @raw ? "true" : "false" def toJSON {type: 'Bool', value: @value} def loc @value:region ? @value.region : [0,0] export class Undefined < Literal def isPrimitive yes def isTruthy no def cache self def c M("undefined",@value) export class Nil < Literal def isPrimitive yes def isTruthy no def cache self def c M("null",@value) export class True < Bool def raw true def isTruthy yes def c M("true",@value) export class False < Bool def raw false def isTruthy no def c M("false",@value) export class Num < Literal # value is token - should not be def initialize v @traversed = no @value = v def toString String(@value).replace(/\_/g,'') def toNumber @number ?= parseFloat(toString) def isPrimitive deep yes def isTruthy toNumber != 0 def negate @value = -toNumber self def shouldParenthesize par = up par isa Access and par.left == self def js o return toString def c o return super(o) if @cache var out = M(toString,@value) var par = STACK.current var paren = par isa Access and par.left == self # only if this is the right part of the access paren ? "({out})" : out def cache o return self unless o and (o:cache or o:pool) super(o) def raw # really? JSON.parse(toString) def toJSON {type: typeName, value: raw} export class NumWithUnit < Literal def initialize v, unit @traversed = no @value = v @unit = unit def negate set(negate: yes) return self def c o let unit = String(@unit) let val = String(@value) val = "-{val}" if option(:negate) if unit == 'ms' val = "{val}" elif unit == 's' val = "({val} * 1000)" elif unit == 'minutes' val = "({val} * 60 * 1000)" elif unit == 'hours' val = "({val} * 60 * 60 * 1000)" elif unit == 'days' val = "({val} * 24 * 60 * 60 * 1000)" elif unit == 'n' val = "{val}n" elif unit == 'fps' val = "(1000 / {val})" else val = "{val}{unit}" val = "'{val}'" unless o and o:unqouted if OPTS:sourcemap and (!o or o:mark !== false) val = M(val,self) return val def endLoc @unit.endLoc export class ExpressionWithUnit < ValueNode def initialize value, unit @value = value @unit = unit def js o let unit = String(@unit) # util.unit(@value,STR(@unit)).c # let out = typeof @value == 'string' ? @value : @value.c "({value.c}+{STR(@unit).c})" # should be quoted no? # what about strings in object-literals? # we want to be able to see if the values are allowed export class Str < Literal def initialize v @traversed = no @expression = yes @cache = null @value = v # should grab the actual value immediately? def isString yes def isPrimitive deep yes def raw # JSON.parse requires double-quoted strings, # while eval also allows single quotes. # NEXT eval is not accessible like this # WARNING TODO be careful! - should clean up @raw ||= String(value).slice(1,-1) # incredibly stupid solution def isValidIdentifier # there are also some values we cannot use raw.match(/^[a-zA-Z\$\_]+[\d\w\$\_]*$/) ? true : false def isTemplate String(@value)[0] == '`' def js o String(@value) def c o @cache ? super(o) : (M js, @value, o) export class TemplateString < ListNode def js let parts = @nodes.map do |node| node isa String ? node : node.c() let out = '`' + parts.join('') + '`' return out export class Interpolation < ValueNode # Currently not used - it would be better to use this # for real interpolated strings though, than to break # them up into their parts before parsing export class InterpolatedString < Node def initialize nodes, o = {} @nodes = nodes @options = o self def add part @nodes.push(part) if part self def visit for node in @nodes node.traverse self def startLoc option(:open).startLoc def endLoc option(:close).endLoc def isString yes def isTemplate String(option(:open)) == '`' def escapeString str str = str.replace(/\n/g, '\\\n') def toArray let items = @nodes.map do |part,i| if part isa Token and part.@type == 'NEOSTRING' Str.new('"' + part.@value + '"') else part return items def js o, opts var kind = String(option("open") or '"') if kind:length == 3 kind = kind[0] # creating the string if opts and opts:as == 'template' var parts = [] @nodes.map do |part,i| if part isa Token and part.@type == 'NEOSTRING' parts.push escapeString(part.@value) elif part parts.push('${',part.c(expression: yes),'}') return '`' + parts.join('') + '`' else var noparen = @noparen var parts = [] var str = noparen ? '' : '(' @nodes.map do |part,i| if part isa Token and part.@type == 'NEOSTRING' parts.push(kind + escapeString(part.@value) + kind) elif part if i == 0 # force first part to be string parts.push('""') part.@parens = yes parts.push(part.c(expression: yes)) str += parts.join(" + ") str += ')' unless noparen return str # Because we've dropped the Str-wrapper it is kinda difficult export class Symbol < Literal def isValidIdentifier raw.match(/^[a-zA-Z\$\_]+[\d\w\$\_]*$/) ? true : false def isPrimitive deep yes def raw @raw ||= AST.sym(value.toString.replace(/^\:/,'')) def js o "'{AST.sym(raw)}'" export class RegExp < Literal def isPrimitive yes def js var v = super # special casing heregex if var m = constants.HEREGEX.exec(v) # console.log 'matxhed heregex',m var re = m[1].replace(constants.HEREGEX_OMIT, '').replace(/\//g, '\\/') return '/' + (re or '(?:)') + '/' + m[2] v == '//' ? '/(?:)/' : v # Should inherit from ListNode - would simplify export class Arr < Literal def load value value isa Array ? ArgList.new(value) : value def push item value.push(item) self def count value:length def nodes var val = value val isa Array ? val : val.nodes def splat value.some(|v| v isa Splat) def visit @value.traverse if @value and @value:traverse self def isPrimitive deep !value.some(|v| !v.isPrimitive(yes) ) def js o var val = @value return "[]" unless val var nodes = val isa Array ? val : val.nodes var out = val isa Array ? AST.cary(val) : val.c out = "[{out}]" if datatype and STACK.tsc out = datatype.c + '(' + out + ')' return out def hasSideEffects value.some(|v| v.hasSideEffects ) def toString "Arr" def indented a,b @value.indented(a,b) self def self.wrap val Arr.new(val) # should not be cklassified as a literal? export class Obj < Literal def load value value isa Array ? AssignList.new(value) : value def visit @value.traverse if @value self def isPrimitive deep !value.some(|v| !v.isPrimitive(yes) ) def js o return '{' + value.c + '}' def add k, v k = Identifier.new(k) if k isa String or k isa Token var kv = ObjAttr.new(k,v) value.push(kv) return kv def remove key for k in value value.remove(k) if k.key.symbol == key self def keys Object.keys(hash) def hash var hash = {} for k in value hash[k.key.symbol] = k.value if k isa ObjAttr return hash # return k if k.key.symbol == key # add method for finding properties etc? def key key for k in value return k if k isa ObjAttr and k.key.symbol == key null def indented a,b @value.indented(a,b) self def hasSideEffects value.some(|v| v.hasSideEffects ) # for converting a real object into an ast-representation def self.wrap obj var attrs = [] for own k,v of obj if v isa Array v = Arr.wrap(v) elif v:constructor == Object v = Obj.wrap(v) # if k isa String # k = LIT(k) v = NODIFY(v) if k isa String k = Identifier.new(k) attrs.push(ObjAttr.new(k,v)) return Obj.new(attrs) def toString "Obj" export class NumberLike < ValueNode def consume node if node == NumberLike or node isa NumberLike return self super def js "({@value.c}).valueOf()" export class ObjAttr < Node prop key prop value prop options def initialize key, value, defaults @traversed = no @key = key @value = value @dynamic = key isa Op @defaults = defaults self def visit stack, state # should probably traverse key as well, unless it is a dead simple identifier key.traverse value.traverse if value @defaults.traverse if @defaults let decl = state && state:declaring if key isa Ivar if !value key = Identifier.new(key.value) value = OP('.',scope__.context,key) if @defaults value = OP('=',value,@defaults) @defaults = null elif key isa Private if !value value = OP('.',scope__.context,key) key = Identifier.new(key.value) elif key isa Identifier # if state && state:declaring # key.variable = scope__.register(key.symbol,key)\ # isnt this rather going to if !value if decl value = scope__.register(key.symbol,key, type: decl) value = value.via(key) if @defaults value = OP('=',value,@defaults) @defaults = null else value = scope__.lookup(key.symbol) unless value value = OP('.',scope__.context,key) self def js o let key = self.key let kjs # if key isa Identifier and String(key.@value)[0] == '@' # key = Ivar.new(key) if key isa IdentifierExpression or key isa SymbolIdentifier # streamline this interface kjs = key.asObjectKey elif key isa InterpolatedString kjs = "[{key.c}]" elif key.isReserved kjs = "'{key.c}'" elif key isa Str and key.isValidIdentifier kjs = key.raw else kjs = key.c(as: 'key') # var k = key.isReserved ? "'{key.c}'" : key.c if @defaults "{kjs} = {@defaults.c}" elif value "{kjs}: {value.c}" else "{kjs}" def hasSideEffects true def isPrimitive deep !@value or @value.isPrimitive(deep) export class ObjRestAttr < ObjAttr def js o let key = self.key if value "...{value.c}" else "...{key.c}" export class ArgsReference < Node # should register in this scope -- def c "arguments" # should be a separate Context or something export class Self < Literal def initialize value @value = value def cache self def reference return self def visit @scope__ = scope__ @scope__.context self def js var s = @scope__ or scope__ s ? s.context.c : "this" def c let out = M(js,@value) let typ = STACK.tsc and option(:datatype) if typ out = "{typ.c}({out})" return out export class This < Self def cache self def reference self def visit self def js "this" # OPERATORS export class Op < Node prop op prop left prop right def initialize o, l, r # set expression yes, no? @expression = no @traversed = no @parens = no @cache = null @invert = no @opToken = o @op = o and o.@value or o if @op == 'and' @op = '&&' elif @op == 'or' @op = '||' elif @op == 'is' @op = '===' elif @op == 'isnt' @op = '!==' elif @op == 'not' @op = '!' @left = l @right = r return self def visit @right.traverse if @right and @right:traverse @left.traverse if @left and @left:traverse return self def hasTagRight if isLogical let l = @left.unwrappedNode let r = @right.unwrappedNode if r isa TagLike return yes if r isa Op and r.hasTagRight return yes if r isa Op and r.hasTagRight return yes return no def opToIfTree if hasTagRight let l = @left.unwrappedNode let r = @right.unwrappedNode if @op == '&&' if l isa Op and l.hasTagRight @left.warn "Tag not allowed here" l = l.opToIfTree if l isa Op r = r.opToIfTree if r isa Op if r isa If r.test = OP('&&',l,r.test) return r return If.new(l,Block.new([r])).traverse elif @op == '||' l = l.opToIfTree if l isa Op if l isa If return l.addElse(Block.new([r])) else return If.new(l,Block.new([])).addElse(Block.new([r])).traverse return self def isExpressable # what if right is a string?!? !right || right.isExpressable def startLoc let l = @left l and l:startLoc ? l.startLoc : super def js o var out = null if STACK.tsc and isBitwise if isAssignment let typ = String(@op).split('=') @op = '=' @right = OP(typ[0],@left,@right) else @right = @right.consume(NumberLike) if @right @left = @left.consume(NumberLike) if @left var op = @op let opv = op var l = @left var r = @right # make the left and right consume valueOf if op == '!&' return "({C l} {M '&',@opToken} {C r})==0" elif op == '??' return "({C l} {M op,@opToken} {C r})" elif op == '|=?' return If.ternary(OP('!&',l,r.cache), Parens.new([OP('|=',l,r),TRUE]), FALSE ).c elif op == '~=?' return If.ternary(OP('&',l,r.cache), Parens.new([OP('~=',l,r),TRUE]), FALSE ).c elif op == '^=?' return OP('!!',OP('&',OP('^=',l,r.cache),r)).c elif op == '=?' r.cache return If.ternary(OP('!=',l,r), Parens.new([OP('=',l,r),TRUE]), FALSE ).c l = l.c if l isa Node r = r.c if r isa Node if l && r out ||= "{l} {M op,@opToken} {r}" elif l let s = @opToken and @opToken:spaced ? ' ' : '' out ||= "{M op,@opToken}{s}{l}" out def isString @op == '+' and @left and @left.isString def isLogical @op == '&&' or @op == '||' def isBitwise !!constants.BITWISE_OPERATORS[@op] def isAssignment !!constants.ASSIGNMENT_OPERATORS[@op] def shouldParenthesize @parens # option(:parens) def precedence 10 def consume node if node == NumberLike if isBitwise return self return super if isExpressable # TODO can rather use global caching? var tmpvar = scope__.declare(:tmp,null,system: yes) var clone = OP(op,left,null) var ast = right.consume(clone) ast.consume(node) if node return ast export class ComparisonOp < Op def invert # are there other comparison ops? # what about a chain? var op = @op var pairs = [ "==","!=" , "===","!==" , ">","<=" , "<",">=" ] var idx = pairs.indexOf(op) idx += (idx % 2 ? -1 : 1) self.op = pairs[idx] @invert = !@invert self def c if left isa ComparisonOp left.right.cache OP('&&',left,OP(op,left.right,right)).c else super def js o var op = @op var l = @left var r = @right l = l.c if l isa Node r = r.c if r isa Node return "{l} {M op,@opToken} {r}" export class UnaryOp < Op def invert if op == '!' return left else super # regular invert def isTruthy var val = AST.truthy(left) return val !== undefined ? (!val) : (undefined) def startLoc let l = (@left or @op) l and l:startLoc ? l.startLoc : @startLoc def js o var l = @left var r = @right var op = op var s = @opToken and @opToken:spaced ? ' ' : '' if op == 'not' op = '!' if op == '!' or op == '!!' # l.@parens = yes var str = l.c var paren = l.shouldParenthesize(self) # FIXME this is a very hacky workaround. Need to handle all this # in the child instead, problems arise due to automatic caching unless (str.match(/^\!?([\w\.]+)$/) or l isa Parens or paren or l isa Access or l isa Call) and !str.match(/[\s\&\|]/) str = '(' + str + ')' # l.set(parens: yes) # sure? "{op}{str}" elif left "{l.c}{s}{op}" else "{op}{s}{r.c}" def normalize return self if op == '!' var node = (left || right).node # for property-accessors we need to rewrite the ast return self def consume node var norm = normalize norm == self ? super : norm.consume(node) def c var norm = normalize norm == self ? super : norm.c export class InstanceOf < Op def js o # fix checks for String and Number if right isa Identifier or right isa VarOrAccess # WARN otherwise - what do we do? does not work with dynamic # classes etc? Should probably send to utility function isa$ var name = AST.c(right.value) var obj = left.node # TODO also check for primitive-constructor if name in ['String','Number','Boolean'] if STACK.tsc return "(typeof {obj.c}=='{name.toLowerCase}')" unless obj isa LocalVarAccess obj.cache # need a double check for these (cache left) - possibly return "(typeof {obj.c}=='{name.toLowerCase}'||{obj.c} instanceof {name})" # convert var out = "{left.c} instanceof {right.c}" # should this not happen in #c? out = helpers.parenthesize(out) if o.parent isa Op out export class TypeOf < Op def js o "typeof {left.c}" export class Delete < Op def js o # TODO this will execute calls several times if the path is not directly to an object # need to cache the receiver var l = left var tmp = scope__.temporary(self, pool: 'val') var o = OP('=',tmp,l) # FIXME return "({o.c},delete {l.c}, {tmp.c})" # oh well # var ast = [OP('=',tmp,left),"delete {left.c}",tmp] # should parenthesize directly no? # ast.c def shouldParenthesize yes export class In < Op def invert @invert = !@invert self def js o # var cond = @invert ? "== -1" : ">= 0" # var idx = Util.indexOf(left,right) var out = util.contains(left,right) "{@invert ? '!' : ''}{out.c}" # ACCESS export class Access < Op def initialize o, l, r # set expression yes, no? @expression = no @traversed = no @parens = no @cache = null @invert = no @op = o and o.@value or o @optok = o @left = l @right = r return self def startLoc (@left or @right).startLoc def endLoc @right && @right.endLoc def clone left, right var ctor = self:constructor ctor.new(op,left,right) def isRuntimeReference if left isa VarOrAccess and left.@variable isa ImbaRuntime if right isa Identifier return right.toString return yes return no # def datatype # right:datatype ? right.datatype : null def js stack var raw = null var lft = left var rgt = right var rgtexpr = null if lft isa VarOrAccess and lft.@variable isa ImportProxy return lft.@variable.access(rgt,lft).c if rgt isa Token rgt = Identifier.new(rgt) var ctx = (lft || scope__.context) var pre = "" var mark = '' let safeop = safechain ? '?' : '' unless @startLoc @startLoc = (lft or rgt).startLoc if lft isa Super and stack.method and stack.method.option('inExtension') and false return CALL( OP('.',scope__.context,'super$'), [rgt isa Identifier ? rgt.toStr : rgt] ).c() if rgt isa Index and rgt.value isa Num rgt = rgt.value if rgt isa Num # FIXME not adding type info if rgt.toNumber < 0 return safeop ? util.optNegIndex(ctx,rgt).c : util.negIndex(ctx,rgt).c return ctx.c + "{safeop ? '?.' : ''}[" + rgt.c + "]" # is this right? Should not the index compile the brackets # or value is a symbol -- should be the same, no? if rgt isa Index and (rgt.value isa Str or rgt.value isa Symbol) rgt = rgt.value # TODO do the identifier-validation in a central place instead if rgt isa Str and rgt.isValidIdentifier raw = rgt.raw elif rgt isa Symbol and rgt.isValidIdentifier raw = rgt.raw elif rgt isa InterpolatedIdentifier rgt = rgt.value elif rgt isa SymbolIdentifier yes elif rgt isa Identifier and rgt.isValidIdentifier raw = rgt.c # really? # var ctx = (left || scope__.context) var out = if raw # see if it needs quoting # need to check to see if it is legal let opjs = STACK.tsc ? M('.',@optok) : '.' ctx ? "{safeop}{opjs}{raw}" : raw else var r = rgt isa Node ? rgt.c(expression: yes, as: 'value') : rgt "{safeop ? '?.' : ''}[{r}]" # console.log "access up {stack.up}" # let typ = datatype let up = stack.up let typ = option(:datatype) if ctx if self isa ImplicitAccess and typ and stack.tsc and !(up isa Block) and false out = '/**@type{any}*/('+ctx.c+')' + out else out = ctx.c + out if self isa ImplicitAccess out = M(out,rgt.@token or rgt.@value) if typ and (!(up isa Assign) or up.right.node == self) if up isa Block and (self isa ImplicitAccess or lft isa Self) out = typ.c + ' ' + out else out = typ.c + '(' + out + ')' out = pre + out if pre out = "({out})" return out def visit let lft = left left.traverse if left right.traverse if right @left ||= scope__.context return def isExpressable true def alias right isa Identifier ? right.alias : super() def safechain String(@op) == '?.' def cache o (right isa Ivar && !left) ? self : super(o) def shouldParenthesizeInTernary @parens or @cache export class ImplicitAccess < Access def datatype super or @right.datatype # Should change this to just refer directly to the variable? Or VarReference export class LocalVarAccess < Access prop safechain def js o if right isa Variable and right.type == 'meth' return "{right.c}()" unless up isa Call return right.c def variable right def cache o = {} super(o) if o:force self def alias variable.@alias or super() export class PropertyAccess < Access def initialize o, l, r @traversed = no @invert = no @parens = no @expression = no # yes? @cache = null @op = o @left = l @right = r return self def visit @right.traverse if @right @left.traverse if @left return self # right in c we should possibly override # to create a call and regular access instead def js o # if var rec = receiver # var ast = CALL(OP('.',left,right),[]) # convert to ArgList or null # ast.receiver = rec # return ast.c var up = up # really need to fix this - for sure # should be possible for the function to remove this this instead? var js = "{super(o)}" return js def receiver if left isa Super SELF else null export class IvarAccess < Access def visit @right.traverse if @right @left ? @left.traverse : scope__.context return self def cache # WARN hmm, this is not right... when accessing on another object it will need to be cached return self export class IndexAccess < Access def cache o = {} return super if o:force right.cache self export class VarAccess < ValueNode export class VarOrAccess < ValueNode def initialize value # should rather call up to valuenode? @traversed = no @parens = no @value = value @identifier = value @token = value.@value @variable = null self def isGlobal name @variable and @variable.isGlobal(name) def startLoc @token.startLoc def endLoc @token.endLoc # Shortcircuit traverse so that it is not added to the stack?! def visit stack, state # @identifier = value # this is not a real identifier? var variable var scope = scope__ var name = value.symbol if state && state:declaring # console.log "VarOrAccess {@identifier}" variable = scope.register(value,self,type: state:declaring) # if name == '$' # if @tagref = stack.up(Tag) # return self variable ||= scope.lookup(value.symbol) if variable and variable isa GlobalReference let name = variable.name if variable isa ZonedVariable and !stack.tsc @value = variable.forScope(scope) elif stack.tsc @value = LIT(name) elif stack.isNode @value = LIT(scope.imba.c) if name != 'imba' @value = LIT("{scope.imba.c}.{name}") else @value = LIT(name) # does not really need to have a declarator already? -- tricky elif variable && variable.declarator # var decl = variable.declarator let vscope = variable.scope # if the variable is not initialized just yet and we are # in the same scope - we should not treat this as a var-lookup # ie. var x = x would resolve to var x = this.x() if x # was not previously defined if vscope == scope and !variable.@initialized # here we need to check if the variable exists outside # if it does - we need to ensure that the inner variable does not collide let outerVar = scope.parent.lookup(value) if outerVar variable.@virtual = yes variable.@shadowing = outerVar variable = outerVar # should do this even if we are not in the same scope? # we only need to be in the same closure(!) if variable and variable.@initialized or (scope.closure != vscope.closure) @variable = variable variable.addReference(self) @value = variable # variable.accessor(self) @token.@variable = variable # if vscope isa RootScope and vscope.context != scope.context and variable.type == 'meth' # warn "calling method from root scope {value} is deprecated - see issue #112" return self # FIX # @value.safechain = safechain elif value.symbol == 'self' @value = scope.context @isSelf = yes elif !@identifier.isCapitalized let selfvar = scope.lookup('self') let ctx = scope.context if !selfvar and ctx.isGlobalContext @includeType = yes else @value = ImplicitAccess.new(".",(Self.new).traverse,@value).set(datatype: datatype) return self def js o if @tagref return @tagref.ref let val = @variable or @value if @variable and @variable.declarator != self if STACK.tsc and val.@typedAlias let out = val.@typedAlias.c return out # if @variable.c == 'val' # # not when we are part of a setter though # console.log 'compile varoraccess variable',@variable let typ = datatype # or (STACK.tsc and @variable.vartype) # FIXME not if it is in left-side of assignment if typ return typ.c() + '(' + @variable.c() + ')' return val.c() def node @variable ? self : value def datatype super or @identifier.datatype def symbol @identifier.symbol def cache o = {} @variable ? (o:force ? super(o) : self) : value.cache(o) def decache @variable ? super() : value.decache self def dom value.dom def safechain @identifier.safechain def dump { loc: loc } def loc var loc = @identifier.region return loc or [0,0] def region @identifier.region def shouldParenthesizeInTernary @cache or (@value and @value.@cache) or @parens def toString "VarOrAccess({value})" def toJSON {type: typeName, value: @identifier.toString} export class VarReference < ValueNode prop variable prop declared prop type def initialize value, type if value isa VarOrAccess value = value.value @variable = null elif value isa Variable @variable = value value = "" # for now - this can happen super(value) @export = no @type = type and String(type) @declared = yes # just testing now def datatype super or (@value:datatype ? @value.datatype : null) def loc @value.region def declare self def consume node # really? the consumed node dissappear? forceExpression self def forceExpression unless @expression == true @expression = true for variable in @variables variable.@type = 'let' variable.@virtual = yes variable.autodeclare() return self def visit stack, state var vars = [] var virtualize = stack let scope = scope__ @variables = scope.captureVariableDeclarations do @value.traverse(declaring: @type, variables: vars) # should happen automatically when traversing via traverse(declaring:...) if @value isa Identifier @value.@variable ||= scope.register(@value.symbol,@value,type: @type, datatype: datatype) return self def js stack,params let out = @value.c() let typ = (STACK.tsc and datatype) # console.log "result of varreference {out}" # console.log "is expression?? {@expression} {params:expression}" if @right let rgt = @right.c(expression: true) if typ rgt = "{typ.c}({rgt})" out += " = {rgt}" if @expression if @value isa Obj out = "({out})" else if STACK.tsc and @variables:length > 1 and @variables.some(do $1.vartype) let kind = @type # or var? let js = '' for item in @variables if item.vartype js += item.vartype.c + ' ' js += "{M(kind,@keyword)} {item.c()};\n" if @value isa Obj out = "({out})" js += "{out}" return js out = "{@type} {out}" if option(:export) out = "{M('export',option(:export))} {out}" if !@right and typ out = typ.c + ' ' + out return out # ASSIGN export class Assign < Op def initialize o, l, r # set expression yes, no? @expression = no @traversed = no @parens = no @cache = null @invert = no @opToken = o @op = o and o.@value or o @left = l @right = r return self def isExpressable !right || right.isExpressable def isUsed # really? # if up is a block in general this should not be used -- since it should already have received implicit self? if up isa Block # && up.last != self return no return yes # FIXME optimize def visit var l = @left var r = @right # The special case where setting `item = item` should compile to `self.item = item` if l isa VarOrAccess and r isa VarOrAccess and l.@identifier.symbol == r.@identifier.symbol @left = l = Access.new(".",scope__.context,l.@value) # console.log "Assign {l} {r}" # Regularly, the var is declared after the right side, so `let item = item` resolves to # `let item = self.item`. In the case of `let item = do ...` however, the variable # must be declared before visiting the inner scope of the function if l isa VarReference and r isa Lambda l.traverse() if r r.traverse(assignment: yes) if l l.traverse() # if l isa VarOrAccess let up = STACK.up if l isa VarReference and !(up isa Block) and !(up isa Export) and !(up isa TagBody) l.forceExpression return self def c o unless right.isExpressable # if left isa VarReference and !(right isa Loop) and false # let ref = left # @left = left.@value # return Block.new([ref,BR,right.consume(self)]).c(o) if left isa VarReference and (!(right isa Loop) or @expression) left.forceExpression return right.consume(self).c(o) # testing this return super(o) def js o unless right.isExpressable p "Assign#js right is not expressable " # here this should be go out of the stack(!) # it should already be consumed? left.forceExpression if left isa VarReference return right.consume(self).c if @expression left.forceExpression var l = left.node var r = right var lc = null if l isa Access and l.left isa Super and false if let m = STACK.method if m.option('inExtension') let key = l.right key = key.toStr if key isa Identifier # return "{scope.__context.c}.super$set('{l.right.c}')" let op = CALL( OP('.',scope__.context,'super$set'), [key,right] ) return op.c(expression: true) # We are setting self(!) # TODO document functionality # FIXME Not supported anymore? if l isa Self var ctx = scope__.context l = ctx.reference if l isa VarReference l.@right = r return l.c # test for typescript namespacing if l isa Access and l.@left.@value == 'OPS' and STACK.tsc lc = "globalThis.{M(helpers.toNamespacedIdentifier('OPS',String(l.@right)),l.@right)}" lc ||= l.c var out = "{lc} {op} {right.c(expression: true)}" if let typ = (datatype or (l and !(l isa VarReference) and l.datatype)) out = typ.c() + ' ' + out if l isa Obj out = "({out})" return out # FIXME op is a token? _FIX_ # this (and similar cases) is broken when called from # another position in the stack, since 'up' is dynamic # should maybe freeze up? def shouldParenthesize par = up @parens or par isa Op && par.op != '=' def consume node if node isa TagLike if right isa TagLike right.set(assign: left) return right.consume(node) else return self if node isa Return and left isa VarReference if STACK.tsc let rgt = @right let vars = @left.@variables let after = vars[0] ? VarAccess.new(vars[0]).consume(node) : node return Block.new([self,BR,after]) # return Block.new([OP('=',sysvar,@right),BR,VarAccess.new(@left.@variable).consume(node)]) left.forceExpression if isExpressable forceExpression return super(node) var ast = right.consume(self) return ast.consume(node) export class PushAssign < Assign prop consumed def register node @consumed ||= [] @consumed.push(node) self def js o "{left.c}.push({right.c})" def consume node return self export class TagPushAssign < PushAssign def js o "{left.c}.push({right.c})" def consume node return self export class ConditionalAssign < Assign export class CompoundAssign < Assign # FIXME can we merge consume and js? def consume node return super if isExpressable var ast = normalize return ast.consume(node) unless ast == self ast = right.consume(self) return ast.consume(node) def normalize var ln = left.node # we dont need to change this at all unless ln isa PropertyAccess return self ln.left.cache if ln.left # TODO FIXME we want to cache the context of the assignment var ast = OP('=',left,OP(op[0],left,right)) ast.toExpression if ast.isExpressable return ast def c var ast = normalize return super if ast == self # otherwise it is important that we actually replace this node in the outer block # whenever we normalize and override c it is important that we can pass on caching # etc -- otherwise there WILL be issues. var up = STACK.current if up isa Block # an alternative would be to just pass up.replace(self,ast) ast.c export class TypeAnnotation < Node def initialize value @value = value self def add item @parts.push(item) def startLoc @value.startLoc + 1 def endLoc @value.endLoc def asParam name "@param \{{asRawType}\} {name}" def asRawType let raw = String(@value).slice(1) raw = raw.replace(/(^|[\[\,])\<([a-z\-\d]+)\>/g) do |m,pre,name| # pre + "InstanceType<" + (TagTypeIdentifier.new(name)).toClassName + ">" pre + (TagTypeIdentifier.new(name)).toClassName raw = raw.replace(/(^|[\[\,])self([\]\,\)\>]|$)/g) do |m,pre,post| pre + "this" + post return M(raw,self) def asIteratorValue wrapDoc(asRawType + '[]') def wrapDoc inner return '/**@type {' + inner + '}*/' def c return '/**@type {' + asRawType + '}*/' # M(String(@value).slice(1),self) # IDENTIFIERS # really need to clean this up # Drop the token? export class Identifier < Node prop safechain prop value prop variable def initialize value if value isa Token @startLoc = value.startLoc @value = load(value) @symbol = null if ("" + value).indexOf("?") >= 0 @safechain = yes # @safechain = ("" + value).indexOf("?") >= 0 self def isStatic yes def toRaw @value.@value or @value def add part IdentifierExpression.new(self).add(part) def references variable @value.@variable = variable if @value self def metaIdentifier Identifier.new('αα' + AST.sym(@value)) def load v return (v isa Identifier ? v.value : v) def traverse # NODES.push(self) self def visit if @value isa Node # console.log "IDENTIFIER VALUE IS NODE" @value.traverse self def region [@value.@loc,@value.@loc + @value.@len] def startLoc @value and @value:startLoc ? @value.startLoc : null def endLoc @value and @value:endLoc ? @value.endLoc : null def loc [startLoc,endLoc] def isValidIdentifier # !STACK.tsc or helpers.isValidIdentifier(symbol) def isReserved @value:reserved or RESERVED_TEST.test(String(@value)) def isPredicate (/\?$/).test(String(@value)) def isCapitalized (/^[A-Z]/).test(String(@value)) def isInternal (/^\$/).test(String(@value)) def symbol @symbol ||= AST.sym(value) def toString String(@value) def toStr return Str.new("'" + symbol + "'") def toAttrString return Str.new("'" + String(@value) + "'") def toJSON toString def alias AST.sym(@value) def js o @variable ? @variable.c() : symbol def c o # console.log 'compiling identifier as',o and o:as,symbol if o if o:as == 'value' # console.log 'compiling identifier as',o:as,symbol return "'{symbol}'" if o:as == 'meta' return "'{symbol}'" if o:as == 'namespaced' and o:ns return M("Σ{o:ns}Σ{symbol}",@token or @value) if o:as == 'field' and !isValidIdentifier return M("['{symbol}']",@token or @value) if o:as == 'key' and !isValidIdentifier return "'{symbol}'" let up = STACK.current return toStr.c if (up isa Util and !(up isa Util.Iterable)) # not all utils let out = js # FIXME should it not always enable? if OPTS:sourcemap and (!o or o:mark !== false) out = M(out, @token or @value) return out def dump { loc: region } def namepath toString def shouldParenthesizeInTernary @parens or @cache def registerVariable type, scope = scope__ @variable = scope.register(symbol,self,type: type) return self def resolveVariable scope = scope__ let variable = scope.lookup(symbol) @variable = variable return self export class DecoratorIdentifier < Identifier def symbol helpers.toValidIdentifier(String(@value)) # "decorator${@value.slice(1)}" def toString symbol export class SymbolIdentifier < Identifier def c o = {} if STACK.tsc return variable.c # return "{@value.slice(0)_$}" let out = variable.c if o:as == 'field' "[{out}]" else out def variable @variable ||= scope__.root.symbolRef(@value.slice(0)) def metaIdentifier scope__.root.symbolRef("__" + @value.slice(0)) def isConstant yes def asObjectKey "[{c}]" def toString c def resolveVariable self def registerVariable self export class MixinIdentifier < Identifier def symbol "mixin${@value.slice(1)}" def traverse o if @traversed return self # should not really look in the scope @mixin = scope__.mixin(@value.slice(1)) # console.log 'found mixin?!',@mixin unless @variable resolveVariable @traversed = yes def c o if o and (o:as == 'string' or o:as == 'substr') let flags = toFlags.map do |f| f isa Variable ? "$\{{f.c}\}" : f.raw let out = flags.join(' ') return o:as == 'string' ? "`{out}`" : out let up = STACK.current return toStr.c if (up isa Util and !(up isa Util.Iterable)) # not all utils let out = js if OPTS:sourcemap and (!o or o:mark !== false) out = M(out, @token or @value) return out def toString symbol def toFlagName # look for variable etc symbol def toFlags return @parts if @parts traverse let v = @variable let parts = [] let part = v while part if part.@declarator isa StyleRuleSet parts.push(STR(part.@declarator.@name)) else parts.push(part) part = part.@parent # for part in vars # if part.@declarator isa StyleRuleSet # parts.push(STR(v.@declarator.@name)) # console.log 'resolvedFlagName',parts return @parts = parts if v and v.@declarator isa StyleRuleSet return v.@declarator.@name return null export class Private < Identifier def symbol @symbol ||= AST.sym('__' + value) def add part IdentifierExpression.new(value).add(part).set(prefix: '__', private: yes) # def c # let up = STACK.current # return toStr.c if up isa Util # return '' + symbol export class TagIdRef < ValueNode def initialize v @value = v isa Identifier ? v.value : v self def js "{scope__.imba.c}.getElementById('{value.c}')" # This is not an identifier - it is really a string # Is this not a literal? # FIXME Rename to IvarLiteral? or simply Literal with type Ivar export class Ivar < Identifier def initialize v @value = v isa Identifier ? v.value : v self def name helpers.dashToCamelCase(@value).replace(/^[\#]/,'') # value.c.camelCase.replace(/^@/,'') def alias name # the @ should possibly be gone from the start? def js o return symbol export class Decorator < ValueNode def name @name ||= @value.js def visit @variable = scope__.lookup(name) @value.@variable ||= @variable unless @variable @value = runtime[name] @call.traverse if @call if option(:params) @params = option(:params) @params.traverse if let block = up block.@decorators ||= [] block.@decorators.push(self) def tscGetter name, content = null let out = @value.c if @params out += "({@params.c(expression: yes)})" else out += "()" if content out += ".wrap({content})" return out def c # should return other places as well... return if STACK.current isa ClassBody let out = @value.c if @params out += ".bind([{@params.c(expression: yes)}])" else out += ".bind([])" return out export class DescriptorPart < Node prop params prop value prop context def initialize value, owner @name = value def visit stack if params params.traverse if value value.traverse self def js if context let op = OP('.',context,@name) if params yes # TODO - implement this op = CALL(op,params) # op = OP('=',op,params.@nodes[0]) elif value op = OP('=',op,value) else op = OP('=',op,TRUE) return op.c export class Descriptor < Node prop name prop value prop params def initialize value, owner if value isa Token @name = @value = DecoratorIdentifier.new(value) else @value = value @value.@parens = yes @chain = [] @special = no @params = null self def isSpecial @special def visit stack let pre = stack.@descriptor stack.@descriptor = self if @name # console.log "Descriptor scope {scope__}" @variable = scope__.lookup(@name.js) @value.@variable ||= @variable unless @variable @value = OP('.',THIS,@name) else @value.traverse if @value # @call.traverse if @call @params.traverse if @params @chain.map(|v| v.traverse ) if @callback = option(:callback) # make sure it is not bound to the actual target @callback.traverse if option(:default) @default = option(:default) if @default isa Literal @literal = @default unless @default isa Func @default = Func.new([],[@default],null,{}) @default.traverse stack.@descriptor = pre def valueIsStatic !value or value.isPrimitive or (value isa Func and !value.nonlocals) def isStatic valueIsStatic def isProxy false def add item, type if item isa ArgList if item.@generated # extract options etc let part = DescriptorPart.new(KEY('configure')) part.params = item @chain.push(@last = part) else # console.log 'add',item.@generated if type == '=' # notify if multiple nodes?! (@last or self).value = item.@nodes[0] else (@last or self).params = item or ListNode.new([]) else @chain.push(@last = DescriptorPart.new(item)) return self def js let ref = scope__.root.declare('desc',null,system: yes) let val = @variable ? Instantiation.new(CALL(@value,params or [])) : (@name ? CALL(@value,params or []) : @value) let parts = AST.blk([]) for item in @chain item.context = ref parts.push(item) if @default parts.add LIT("{ref.c}.default = {@default.c}") if @literal parts.add LIT("{ref.c}.default.literal = {@literal.c}") if @callback parts.add LIT("{ref.c}.callback = {@callback.c}") parts.add(ref) if STACK.tsc return "((self,{ref.c}={val.c(mark: yes)})=>({parts.c}))(this)" else parts.unshift(OP('=',ref,val)) return '(' + parts.c(expression: yes) + ')' # Ambiguous - We need to be consistent about Const vs ConstAccess # Becomes more important when we implement typeinference and code-analysis export class Const < Identifier def symbol # console.log "Identifier#symbol {value}" @symbol ||= AST.sym(value) def js o @variable ? @variable.c : symbol def traverse if @traversed return self @traversed = true var curr = STACK.current if !(curr isa Access) or curr.left == self if symbol == "Imba" @variable = scope__.imba else @variable = scope__.lookup(value) self def c if option(:export) "exports.{@value} = " + js else super export class TagTypeIdentifier < Identifier prop name prop ns def initialize value @token = value @value = load(value) self def startLoc @token?.startLoc def endLoc @token?.endLoc def toFunctionalType let sym = Identifier.new(@token) sym = VarOrAccess.new(sym) unless isClass return sym def load val @str = ("" + val) var parts = @str.split(":") @raw = val @name = parts.pop @ns = parts.shift # if any? return @str def traverse o if @traversed return self # NODES.push(self) @traversed = yes if isClass if o and o:declaring registerVariable('const',o:declscope or STACK.scope) if @variable @variable.value = o:declaring else resolveVariable self def js o return "'" + toNodeName + "'" def c js def func var name = @name.replace(/-/g,'_').replace(/\#/,'') name += "${@ns.toLowerCase}" if @ns name def nativeCreateNode let doc = scope__.root.document.c if isSVG CALL(LIT("{doc}.createElementNS"),[STR("http://www.w3.org/2000/svg"),STR(name)]) else CALL(LIT("{doc}.createElement"),[STR(name)]) def isClass !!@str.match(/^[A-Z]/) def isLowerCase !@name.match(/^[A-Z]/) def isNative !@ns and TAG_TYPES.HTML.indexOf(@str) >= 0 def isNativeHTML (!@ns or @ns == 'html') and TAG_TYPES.HTML.indexOf(@name) >= 0 def isNativeSVG @ns == 'svg' and TAG_TYPES.SVG.indexOf(@str) >= 0 def isSVG @ns == 'svg' or (!isNative and !@ns and TAG_NAMES["svg_{@str}"]) def isAsset false def toAssetName isAsset ? @str : null # .slice(4) def symbol @str def isCustom !isNative and !isNativeSVG def isComponent !isNative and !isNativeSVG def toSelector toNodeName def resolveVariable scope = scope__ let variable = scope__.lookup(@str) @variable = variable return self def toVarPrefix let str = @str str.replace(/[\:\-]/g,'') def toExtensionName "Γ{helpers.toValidIdentifier(@str)}" def toClassName let str = @str if str == 'element' return 'Element' elif str == 'component' return 'imba.Component' elif str == 'svg:element' return 'SVGElement' elif str == 'htmlelement' return 'HTMLElement' elif str == 'fragment' return 'DocumentFragment' let match = TAG_NAMES[isSVG ? "svg_{@name}" : @name] return match:name if match if @str == 'fragment' return 'DocumentFragment' elif isClass return @str elif STACK.tsc return "Γ{helpers.toValidIdentifier(@str)}" return helpers.pascalCase(@str + '-custom-element') return @str.replace(/\-/g,'_') + '$$TAG$$' else return helpers.pascalCase(@str + '-component') def toTscName @str.replace(/\-/g,'_') + '$$TAG$$' def sourceId @sourceId ||= STACK.sourceId + '-' + STACK.generateId('tag') def toNodeName if isClass @nodeName ||= helpers.dasherize(@str + '-' + sourceId) else @str def toTypeArgument if @variable @variable.c else name def id var m = @str.match(/\#([\w\-\d\_]+)\b/) m ? m[1] : null def flag "_" + name.replace(/--/g,'_').toLowerCase def sel ".{flag}" # + name.replace(/-/g,'_').toLowerCase def string value def toString value export class InterpolatedIdentifier < ValueNode def js "[{value.c}]" export class Argvar < ValueNode def c # NEXT -- global.parseInt or Number.parseInt (better) var v = parseInt(String(value)) # FIXME Not needed anymore? I think the lexer handles this var out = "arguments" if v > 0 var s = scope__ # params need to go up to the closeste method-scope var par = s.params.at(v - 1,yes) out = "{AST.c(par.name)}" # c M(out, @token or @value) # CALL export class DoPlaceholder < Node export class TaggedTemplate < Node prop value prop string def initialize value,string @value = value @string = string def visit @value.traverse if @value isa Node @string.traverse if !@string.isTemplate @string.warn("Only `` strings allowed in template literals") self def js @value.c + @string.c(as: 'template') # @value.c + TemplateString.new(@string.@nodes).c(as: 'template') # def c # "HELLO" export class Call < Node prop callee prop receiver prop args prop block # def m # console.log "called Call.m {@callee} ({startLoc} - {@callee.startLoc}) [{endLoc}]" # super def initialize callee, args, opexists @traversed = no @expression = no @parens = no @cache = null @receiver = null @opexists = opexists # some axioms that share the same syntax as calls will be redirected from here if callee isa BangCall callee = callee.@callee if callee isa Super callee.args = self isa BangCall ? [] : args return callee if callee isa VarOrAccess var str = callee.value.symbol if str == 'new' console.log 'calling' # if str == '__pure__' # console.log 'PUREPRERE' if str == 'extern' callee.value.value.@type = 'EXTERN' return ExternDeclaration.new(args) if str == 'tag' # console.log "ERROR - access args by some method" return TagWrapper.new(args and args:index ? args.index(0) : args[0]) if str == 'export' return Export.new(args) @callee = callee @args = args or ArgList.new([]) if args isa Array @args = ArgList.new(args) if callee isa Decorator callee.@call = self return callee return self def loc @callee.loc def visit args.traverse callee.traverse # if the callee is a PropertyAccess - better to immediately change it let runref = callee.isRuntimeReference if callee isa Access and callee.left.isGlobal('import') let arg = args.first let kind = callee.right.toString if arg isa Str # TODO remove need for ImbaAsset type # callee = runtime:asset if STACK.tsc and false # (false or arg.raw == './ns') yes callee = callee.left else callee = LIT('') let asset = STACK.root.registerAsset(arg.raw,"{kind}",self,arg) args.replace(arg,asset:ref) elif callee.isGlobal('import') # TODO support interpolated strings that can be globbed let arg = args.first let path = arg isa Str and arg.raw # console.log 'calling the global import!!',path if path let ext = path.split('.').pop() # TODO only allow specific kinds after ? if EXT_LOADER_MAP[ext] or path.indexOf('?') >= 0 @asset = STACK.root.registerAsset(path,'',self,arg) args.replace(arg,@asset:ref) elif callee.isGlobal('require') let arg = args.first let path = arg isa Str and arg.raw if runref == 'asset' let arg = args.first if arg isa Str let asset = STACK.root.registerAsset(arg.raw,'asset',self) args.replace(arg,asset:ref) # console.log 'visit Call asset',runref @block && @block.traverse if self isa BangCall and (@args.count == 0) and option(:keyword) let bang = option(:keyword) @args.setEnds(bang,bang) # @args.@startLoc = option(:keyword).startLoc # @args.@endLoc = option(:keyword).endLoc self def addBlock block var pos = @args.filter(|n,i| n isa DoPlaceholder )[0] pos ? args.replace(pos,block) : args.push(block) self def receiver @receiver ||= (callee isa Access && callee.left || NULL) # check if all arguments are expressions - otherwise we have an issue def safechain callee.safechain # really? def shouldParenthesizeInTernary @parens or safechain or @cache def startLoc @startLoc or @callee and @callee:startLoc ? @callee.startLoc : 0 def endLoc @endLoc or (@args and @args.endLoc) or @callee.endLoc def js o if @asset return @asset:ref.c var opt = expression: yes var rec = null # var args = AST.compact(args) # really? var args = args # drop this? var splat = args.some do |v| v isa Splat var out = null var lft = null var rgt = null var wrap = null var callee = @callee = @callee.node # drop the var or access? # if callee isa Call && callee.safechain # yes if callee isa Access lft = callee.left rgt = callee.right if callee isa Super if let m = STACK.method # console.log "in method {m} {m.name} {m.option('inExtension')}" if m.option('inExtension') callee = OP('.',callee,m.name) @receiver = scope__.context # callee = @callee = OP('.',scope__.context,'super$') # args.unshift(m.name.toStr) # = ArgList.new # @receiver = scope__.context self # return "supercall" # never call the property-access directly? if callee isa PropertyAccess # && rec = callee.receiver @receiver = callee.receiver callee = @callee = Access.new(callee.op,callee.left,callee.right) if rgt isa Identifier and rgt.value == 'assert' and !splat and false let arg = args.first arg.option(:assertion,yes) args.@nodes[0] = AssertionNode.new(arg) # console.log "Special assert call!! {arg.c}",arg isa Op # rewrite a.len(..) to len$(a) let safeop = '' if callee isa Access and callee.op == '?.' safeop = '?.' # if callee.safechain # console.log 'function safechain!!' # var isfn = Util.IsFunction.new([callee]) # wrap = ["{isfn.c} && ",""] # callee = OP('.',callee.left,callee.right) # # callee should already be cached now - # should just force expression from the start, no? if @receiver # quick workaround @receiver.cache unless @receiver isa ScopeContext args.unshift(receiver) # should rather rewrite to a new call? out = "{callee.c(expression: yes)}.call({args.c(expression: yes,mark: false)})" else let outargs = "({args.c(expression: yes,mark: false)})" out = "{callee.c(expression: yes)}{safeop}{M(outargs,@args)}" if wrap # we set the cachevar inside if @cache @cache:manual = yes out = "({cachevar.c}={out})" out = [wrap[0],out,wrap[1]].join("") return out export class BangCall < Call export class Instantiation < ValueNode def self.for value, keyword if value isa Tag return value.set(unmemoized: keyword) else return self.new(value).set(keyword: keyword) def js o "{M('new',keyword)} {value.c}" export class New < Call # def startLoc # keyword ? keyword.startLoc : super def visit keyword.warn 'Value.new is deprecated - use new Value' super def endLoc keyword and keyword.endLoc or super def startLoc null def js o var target = callee while target isa Access let left = target.left if (left isa PropertyAccess) or (left isa VarOrAccess) callee.@parens = yes break target = left # var out = "{M(keyword or 'new',keyword)} {callee.c}" var out = "{M('new',keyword)} {M(callee.c,callee)}" out += '()' unless (o.parent isa Call or o.parent isa BangCall) out export class ExternDeclaration < ListNode def visit nodes = map do |item| item.node # drop var or access really # only in global scope? var root = scope__ for item in nodes var variable = root.register item.symbol, item, type: 'global' variable.addReference(item) self def c "// externs" # FLOW export class ControlFlow < Node def loc @body ? @body.loc : [0,0] export class ControlFlowStatement < ControlFlow def isExpressable no export class If < ControlFlow prop test prop body prop alt prop scope prop prevIf def self.ternary cond, body, alt # prefer to compile it this way as well var obj = If.new(cond, Block.new([body]), type: '?') obj.addElse Block.new([alt]) return obj def addElse add if alt && alt isa If alt.addElse(add) else self.alt = add if add isa If add.prevIf = self self def initialize cond, body, o = {} setup @test = cond # (o:type == 'unless' ? UnaryOp.new('!',cond,null) : cond) @body = body @alt = null @type = o:type invert if @type == 'unless' @scope = IfScope.new(self) self def loc @loc ||= [@type ? @type.@loc : 0,body.loc[1]] def invert if @test isa ComparisonOp @test = @test.invert else @test = UnaryOp.new('!',@test,null) def visit stack var alt = alt var scop = @scope scop.visit if scop if test # make sure variables are registered in outer scope @scope = null test.traverse @scope = scop @tag = stack.@tag # console.log "vars in if",Object.keys(@scope.varmap) # TODO deal with variable scope for own name, variable of @scope.varmap if variable.type == 'let' # console.log "variable virtualize" variable.@virtual = yes variable.autodeclare() # the let-variables declared in if(*test*) should be # local to the inner scope, but will technically be # declared in the outer scope. Must get unique name if !stack.isAnalyzing and !stack.tsc @pretest = AST.truthy(test) if @pretest === true # only collapse if condition includes compiletime flags? alt = @alt = null if test isa EnvFlag @preunwrap = true elif @pretest === false loc # cache location before removing body body = null body.traverse if body # should skip the scope in alt. if alt STACK.pop(self) alt.@scope ||= BlockScope.new(alt) alt.traverse STACK.push(self) # force it as expression? toExpression if @type == '?' and isExpressable self def js o var body = body # would possibly want to look up / out var brace = braces: yes, indent: yes if @pretest === true and @preunwrap # what if it is inside expression? let js = body ? body.c(braces: !!prevIf) : 'true' unless prevIf js = helpers.normalizeIndentation(js) if o.isExpression js = '(' + js + ')' return js elif @pretest === false and false alt.prevIf = prevIf if alt isa If let js = alt ? alt.c(braces: !!prevIf) : '' unless prevIf js = helpers.normalizeIndentation(js) return js if o.isExpression if test?.shouldParenthesizeInTernary test.@parens = yes var cond = test.c(expression: yes) # the condition is always an expression var code = body ? body.c : 'true' # (braces: yes) if body and body.shouldParenthesizeInTernary code = '(' + code + ')' # if code.indexOf(',') >= 0 if alt var altbody = alt.c if alt.shouldParenthesizeInTernary altbody = '(' + altbody + ')' return "{cond} ? {code} : {altbody}" else # again - we need a better way to decide what needs parens # maybe better if we rewrite this to an OP('&&'), and put # the parens logic there # cond should possibly have parens - but where do we decide? if @tag return "{cond} ? {code} : void(0)" else return "{cond} && {code}" else # if there is only a single item - and it is an expression? var code = null var cond = test.c(expression: yes) # the condition is always an expression # if body.count == 1 # dont indent by ourselves? if body isa Block and body.count == 1 and !(body.first isa LoopFlowStatement) body = body.first # if body.count == 1 # p "one item only!" # body = body.first code = body ? body.c(braces: yes) : '{}' # (braces: yes) # don't wrap if it is only a single expression? var out = "{M 'if',@type} ({cond}) " + code # ' {' + code + '}' # '{' + code + '}' out += " else {alt.c(alt isa If ? {} : brace)}" if alt out def shouldParenthesize !!@parens def consume node if node isa TagLike # now we are reconsuming this node.flag(F.TAG_HAS_BRANCHES) if node.body == self let branches = @body ? [@body] : [] let alt = @alt while alt isa If branches.push(alt.@body) if alt.@body alt = alt.@alt if alt branches.push(alt) for block,i in branches node.@branches.push([]) block.consume(node) return self if node isa TagLoopFragment if @body @body = @body.consume(node) if @alt @alt = @alt.consume(node) return self else return node.register(self) return self if node isa TagPushAssign or node isa TagFragment node.register(self) @body = @body.consume(node) if @body @alt = @alt.consume(node) if @alt return self # special case for If created from conditional assign as well? # @type == '?' and # ideally we dont really want to make any expression like this by default var isRet = node isa Return # might have been forced to expression already # if it was originally a ternary - why not if @expression or ((!isRet or @type == '?') and isExpressable) toExpression # mark as expression(!) - is this needed? return super(node) else @body = @body.consume(node) if @body @alt = @alt.consume(node) if @alt self def isExpressable # process:stdout.write 'x' var exp = (!body || body.isExpressable) && (!alt || alt.isExpressable) return exp export class Loop < Statement prop scope prop options prop body prop catcher prop elseBody def loc var a = @options:keyword var b = @body if a and b # FIXME does not support POST_ variants yet [a.@loc,b.loc[1]] else [0,0] def initialize options = {} @traversed = no @options = options @body = null self def set obj @options ||= {} var keys = Object.keys(obj) for k in keys @options[k] = obj[k] self def addBody body self.body = AST.blk(body) self def addElse block self.elseBody = block self def isReactive @tag and @tag.fragment.isReactive def isStatementLike yes def c o var s = stack var curr = s.current if stack.isExpression or isExpression # what the inner one should not be an expression though? # this will resut in an infinite loop, no?!? scope.closeScope var ast = CALL(FN([],[self]),[]) return ast.c o elif stack.current isa Block or (s.up isa Block and s.current.@consumer == self) super.c o elif @tag super.c 0 else scope.closeScope var ast = CALL(FN([],[self]),[]) # scope.context.reference return ast.c o # need to wrap in function export class While < Loop prop test def initialize test, opts @traversed = no @test = test @options = opts or {} @scope = WhileScope.new(self) # set(opts) if opts if option(:invert) # "invert test for while {@test}" @test = test.invert # invert the test def visit scope.visit test.traverse if test body.traverse if body def loc var o = @options helpers.unionOfLocations(o:keyword,@body,o:guard,@test) # TODO BUG -- when we declare a var like: while var y = ... # the variable will be declared in the WhileScope which never # force-declares the inner variables in the scope def consume node # This is never expressable, but at some point # we might want to wrap it in a function (like CS) return super if isExpressable var reuse = no # WARN Optimization - might have untended side-effects # if we are assigning directly to a local variable, we simply # use said variable for the inner res # if reuse # resvar = scope.declare(node.left.node.variable,Arr.new([]),proxy: yes) # node = null # p "consume variable declarator!?".cyan # else # declare the variable we will use to soak up results # TODO Use a special vartype for this? var resvar = scope.declare('res',Arr.new([]),system: yes) # WHAT -- fix this -- @catcher = PushAssign.new("push",resvar,null) # the value is not preset # what body.consume(@catcher) # should still return the same body # scope vars must not be compiled before this -- this is important var ast = Block.new([self,resvar.accessor]) # should be varaccess instead? ast.consume(node) # NOTE Here we can find a way to know wheter or not we even need to # return the resvar. Often it will not be needed # FIXME what happens if there is no node?!? def js o var out = "while ({test.c(expression: yes)})" + body.c(braces: yes, indent: yes) # .wrap if scope.vars.count > 0 out = scope.vars.c + ';' + out # [scope.vars.c,out] out # This should define an open scope # should rather export class For < Loop def initialize o = {} @traversed = no @options = o @scope = ForScope.new(self) @catcher = null def loc var o = @options helpers.unionOfLocations(o:keyword,@body,o:guard,o:step,o:source) def ref @ref || "{@tag.fragment.cvar}.{oid}" def visit stack scope.visit var parent = stack.@tag options:source.traverse # what about awakening the vars here? # add guard to body if options:guard var op = IF(options:guard.invert,Block.wrap([ContinueStatement.new("continue")])) body.unshift(op,BR) declare if options:await var fnscope = stack.up(Func) # do |item| item isa MethodDeclaration or item isa Fun if fnscope set(native: yes) fnscope.set(async: yes) # TODO Throw if for loop is not for of # TODO Throw if inside tag tree - await not supported there # here add the things to declare if parent # TODO remove @tag = parent stack.@tag = self @level = (@tag && @tag.@level or 0) + 1 body.traverse stack.@tag = parent self def isBare src src and src.@variable and src.@variable.@isArray def declare var o = options var scope = scope var src = o:source var vars = o[:vars] = {} var oi = o:index var params = o:params var bare = isBare(src) # if the name parameter is array or object we move this inside? # what about a range where we also include an index? if src isa Range let from = src.left let to = src.right let dynamic = from !isa Num or to !isa Num if to isa Num vars:len = to else # vars:len = scope.vars.push(vars:index.assignment(src.left)) # vars:len = to.cache(force: yes, pool: 'len').predeclare vars:len = scope.declare('len',to,type: 'let') # to.cache(force: yes, pool: 'len').predeclare # scope.vars.push(vars:index.assignment(src.left)) vars:value = scope.declare(o:name,from,type: 'let') vars:value.addReference(o:name) if o:name if o:index vars:index = scope.declare(o:index,0,type: 'let') vars:index.addReference(o:index) else vars:index = vars:value if dynamic vars:diff = scope.declare('rd',OP('-',vars:len,vars:value),type: 'let') else if oi vars:index = scope.declare(oi,0,type: 'let') else vars:index = scope.declare('i',Num.new(0),system: yes, type: 'let', pool: 'counter') vars:source = bare ? src : scope.declare('items',util.iterable(src),system: yes, type: 'let', pool: 'iter') if params[2] vars:len = scope.declare(params[2],util.len(vars:source),type: 'let') else vars:len = scope.declare('len',util.len(vars:source),type: 'let', pool: 'len', system: yes) if o:name let op = OP('.',vars:source,vars:index).set(datatype: o:name.datatype) o:name.set(datatype: undefined) let decl = VarDeclaration.new(o:name,op,'let') body.unshift(decl,BR) vars:index.addReference(oi) if oi return self def consume node if node isa TagLike return node.register(self) if isExpressable return super if @resvar var ast = Block.new([self,BR,@resvar.accessor]) ast.consume(node) return ast var resvar = null var reuseable = no var assignee = null # this should be autodeclared no? resvar = @resvar ||= scope.register(:res,null,system: yes, type: 'var') @catcher = PushAssign.new("push",resvar,null) # the value is not preset let resval = Arr.new([]) body.consume(@catcher) # should still return the same body resvar.autodeclare() # only if it is a system variable? if node isa VarDeclaration or node isa Assign node.right = resvar.accessor # let block = [self,BR,node] return Block.new([ OP('=',resvar,resval) BR, self, BR, node ]) elif node let block = [OP('=',resvar,resval),BR,self,BR,resvar.accessor.consume(node)] return Block.new(block) return self def js o var vars = options:vars var idx = vars:index var val = vars:value var src = options:source var cond var final if src isa Range let a = src.left let b = src.right let inc = src.inclusive cond = OP(inc ? '<=' : '<',val,vars:len) final = OP('++',val) if vars:diff cond = If.ternary( OP('>',vars:diff,Num.new(0)), cond, OP(inc ? '>=' : '>',val,vars:len)) final = If.ternary( OP('>',vars:diff,Num.new(0)),OP('++',val),OP('--',val)) if idx and idx != val final = ExpressionBlock.new([final,OP('++',idx)]) else cond = OP('<',idx,vars:len) if options:step final = OP('=',idx,OP('+',idx,options:step)) else final = OP('++',idx) var before = "" var after = "" var code = body.c(braces: yes, indent: yes) var head = "{M('for',keyword)} ({scope.vars.c}; {cond.c(expression: yes)}; {final.c(expression: yes)}) " return before + head + code + after export class ForIn < For export class ForOf < For prop source def declare var o = options var vars = o:vars = {} var params = o:params var k var v # possibly proxy the index-variable? if o:own vars:source = o:source.@variable || scope.declare('o',o:source, system: true, type: 'let') o:value = o:index var i = vars:index = scope.declare('i',Num.new(0),system: yes, type: 'let', pool: 'counter') # systemvariable -- should not really be added to the map var keys = vars:keys = scope.declare('keys',Util.keys(vars:source.accessor),system: yes, type: 'let') # the outer one should resolve first var l = vars:len = scope.declare('l',Util.len(keys.accessor),system: yes, type: 'let') k = vars:key = scope.declare(o:name,null,type: 'let') # scope.declare(o:name,null,system: yes) if o:value isa Obj or o:value isa Arr body.unshift(VarDeclaration.new(o:value,OP('.',vars:source,k),'let'),BR) vars:value = null elif o:value v = vars:value = scope.declare(o:value,null,let: yes, type: 'let') else source = vars:source = STACK.tsc ? o:source : util.iterable(o:source) vars:value = o:value = o:name # need to visit these to declare them let declvars = scope__.captureVariableDeclarations do o:value.traverse(declaring: 'let') if o:value isa Identifier o:value.@variable ||= scope__.register(o:value.symbol,o:value,type: 'let') @declvars = declvars # need to declare this variable outside the for of if o:index vars:counter = scope.parent.temporary(null,{},"{o:index}$") body.unshift(VarDeclaration.new(o:index,OP('++',vars:counter),'let'),BR) self if params[2] params[2].warn("Length parameter only allowed on for-in loops") # TODO use util - why add references already? Ah -- this is for the highlighting v.addReference(o:index) if v and o:index k.addReference(o:name) if k and o:name self def js o var vars = options:vars var osrc = options:source var src = vars:source var k = vars:key var v = vars:value var i = vars:index var code if options:own # FIXME are we sure about this? if v and v.refcount > 0 body.unshift(OP('=',v,OP('.',src,k))) body.unshift(OP('=',k,OP('.',vars:keys,i))) code = body.c(indent: yes, braces: yes) # .wrap var head = "{M('for',keyword)} ({scope.vars.c}; {OP('<',i,vars:len).c}; {OP('++',i).c})" return head + code else if STACK.tsc # get all the variables declared in the let for item in @declvars if item.vartype let vname = item.c let decl = item.@declarator let op = LIT("let {M item.typedAlias.c,decl} = {item.vartype.c}({vname})") body.unshift(op) # compile to a naive for of loop code = scope.c(braces: yes, indent: yes) # let inCode = osrc.@variable ? src : (OP('=',src,osrc)) # it is really important that this is a treated as a statement let ofjs = src.c(expression: yes) let js = "(let {v.c} of {ofjs})" + code if options:await js = "{M('await',options:await)} {js}" js = "{M('for',keyword)} {js}" if vars:counter js = "{vars:counter} = 0; {js}" return js def head var v = options:vars # skipping head? [ OP('=',v:key,OP('.',v:keys,v:index)) OP('=',v:value,OP('.',v:source,v:key)) if v:value ] # NO NEED? export class Begin < Block def initialize body @nodes = AST.blk(body).nodes def shouldParenthesize isExpression export class Switch < ControlFlowStatement prop source prop cases prop fallback def initialize a,b,c @traversed = no @source = a @cases = b @fallback = c def visit c.traverse for c in cases fallback.traverse if fallback source.traverse if source return def consume node if node isa TagLike if node.body == self let branches = @cases.slice(0).concat([@fallback]) for block,i in branches when block node.@branches.push([]) block.consume(node) return self return node.register(self) # TODO work inside tags (like loops) @cases = @cases.map(|item| item.consume(node)) @fallback = @fallback.consume(node) if @fallback self def c o if stack.isExpression or isExpression var ast = CALL(FN([],[self]),[]) return ast.c o super.c(o) def js o var body = [] for part in cases part.autobreak body.push(part) if fallback body.push("default:\n" + fallback.c(indent: yes)) "switch ({source.c}) " + helpers.bracketize(AST.cary(body).join("\n"),yes) export class SwitchCase < ControlFlowStatement prop test prop body def initialize test, body @traversed = no @test = test @body = AST.blk(body) @scope = BlockScope.new(self) def visit scope__.visit body.traverse def consume node body.consume(node) self def autobreak body.push(BreakStatement.new) unless body.last isa BreakStatement self def js o @test = [@test] unless @test isa Array var cases = @test.map do |item| "case {item.c}: " cases.join("\n") + body.c(indent: yes, braces: yes) export class Try < ControlFlowStatement prop body # prop ncatch # prop nfinally def initialize body, c, f @traversed = no @body = AST.blk(body) @catch = c @finally = f def consume node @body = @body.consume(node) @catch = @catch.consume(node) if @catch @finally = @finally.consume(node) if @finally self def visit @body.traverse @catch.traverse if @catch @finally.traverse if @finally # no blocks - add an empty catch def js o var out = "try " + body.c(braces: yes, indent: yes) out += " " + @catch.c if @catch out += " " + @finally.c if @finally unless @catch or @finally out += " catch (e) \{ \}" out += ";" out export class Catch < ControlFlowStatement prop body def initialize body, varname @traversed = no @body = AST.blk(body or []) @scope = CatchScope.new(self) @varname = varname self def consume node @body = @body.consume(node) self def visit @scope.visit @variable = @scope.register(@varname,self,type: 'let', pool: 'catchvar') if @body.len == 0 # @variable.datatype = 'Something' let node = @variable.accessor let accessor = node if STACK.tsc node = IF(LIT("{node.c} instanceof Error"),node) @body.push(node) @body.traverse def js o # only indent if indented by default? "catch ({@variable.c}) " + @body.c(braces: yes, indent: yes) # repeating myself.. don't deal with it until we move to compact tuple-args # for all astnodes export class Finally < ControlFlowStatement def initialize body @traversed = no @body = AST.blk(body or []) def visit @body.traverse def consume node # swallow silently self def js o "finally " + @body.c(braces: yes, indent: yes) # RANGE export class Range < Op def inclusive op == '..' def c "range" export class Splat < ValueNode def js o return "...{value.c}" var par = stack.parent if par isa ArgList or par isa Arr "Array.from({value.c})" else p "what is the parent? {par}" "SPLAT" def node value # TAGS export class IdentifierExpression < Node prop single def self.wrap node return node node isa self ? node : self.new(node) def initialize value super @static = yes @nodes = [@single = value] def add part @nodes.push(part) @single = null self def isPrimitive @single and @single isa Token def isStatic isPrimitive def visit for node in @nodes when node isa Node node.traverse self def asObjectKey if isPrimitive "{@single.c()}" elif @single "[{@single.c()}]" else "[{asString}]" def startLoc let n = @nodes[0] n?.startLoc def endLoc let n = @nodes[@nodes:length - 1] n?.endLoc def asIdentifier @single ? "[{@single.c()}]" : "[{asString}]" def asString # what if a part is a string? let s = '`' if option(:prefix) s += option(:prefix) for node in @nodes if node isa Token s += node.value else s += '${' s += node.c() s += '}' s += '`' return s def toRaw @single ? @single.c : '' def toString toRaw def js s, o = {} if o:as == 'string' or s.parent isa Util return asString elif o:as == 'key' return asObjectKey elif o:as == 'access' yes elif @single and @single isa Node return @single.c(o) else asString export class TagPart < Node prop name prop value prop params def initialize value, owner @name = load(value) @tag = owner @chain = [] @special = no @params = null self def load value value def isSpecial @special def visit @chain.map(|v| v.traverse ) @value.traverse if @value @name.traverse if @name:traverse self def quoted @quoted ||= (@name isa IdentifierExpression ? @name.asString : helpers.singlequote(@name)) def valueIsStatic !value or value.isPrimitive or (value isa Func and !value.nonlocals) def isStatic valueIsStatic def isProxy false def add item, type if type == TagArgList (@last or self).params = item or ListNode.new([]) else @chain.push(@last = TagModifier.new(item)) return self def modifiers @modifiers ||= TagModifiers.new(@chain).traverse def js "" def ref "c$.{oid}" def tagRef @tagRef or @tag.ref export class TagId < TagPart def js "{tagRef}.id={quoted}" export class TagFlag < TagPart prop condition def rawClassName name.toRaw def value @name def visit @chain.map(|v| v.traverse ) # @value.traverse if @value @condition.traverse if @condition @name.traverse if @name:traverse def isStatic !isConditional and (@name isa Token or @name.isStatic or @name isa MixinIdentifier) def isConditional !!condition def js if STACK.tsc let val = value.c return condition ? "[{val},{condition.c}]" : "[{val}]" # NOT used anymore let val = value.c(as: 'string') condition ? "{tagRef}.flags.toggle({val},{condition.c})" : "{tagRef}.classList.add({val})" export class TagSep < TagPart export class TagArgList < TagPart export class TagAttr < TagPart def isSpecial String(@name) == 'value' def startLoc @name?.startLoc def endLoc @value?.endLoc def isStatic super and @chain.every(do |item| let val = item isa Parens ? item.value : item val isa Func ? !val.nonlocals : val.isPrimitive ) def visit @chain.map(|v| v.traverse ) @value.traverse if @value @name.traverse if @name:traverse let key = @key = String(@name) let i = key.indexOf(':') if i >= 0 @ns = key.slice(0,i) @key = key.slice(i + 1) unless @value @autovalue = yes @value = STR(key) if @chain:length @mods = {} for m in @chain @mods[m.name] = 1 if @ns == 'bind' STACK.use('dom_bind') if !@ns and @key == 'ease' STACK.use('dom_transitions') # console.log 'visit tag attr',@tag and @tag.tagName let isAsset = key == 'asset' or (key == 'src' and value isa Str and (/^(style|img|script|svg)$/).test(@tag.tagName)) if isAsset let tagName = @tag.tagName let kind = 'asset' if tagName == 'svg' # kind = 'js' # no kind here? kind = '' elif tagName == 'img' kind = 'img' elif tagName == 'script' kind = 'web' elif tagName == 'style' kind = 'css' let path = value isa Str and value.raw if path and !path.match(/^(\/|https?\:\/\/)/) @asset = STACK.root.registerAsset(path,kind,self,value) self def ns @ns def key @key def mods @mods def nameIdentifier @nameIdentifier ||= Identifier.new(key) def modsIdentifier @modsIdentifier ||= Identifier.new(key + '__') def js o # let mods = AST.compileRaw(@mods or null) let val = value.c(o) let bval = val let op = M('=',option(:op)) let isAttr = (key.match(/^(aria-|data-)/) or key == 'style') or (@tag and @tag.isSVG) or ns == 'html' let tagName = @tag and @tag.@tagName let tref = @tag.ref if @asset val = @asset:ref.c if STACK.tsc and (isAttr or TAG_GLOBAL_ATTRIBUTES[key]) return "{tref}.setAttribute('{key}',String({val}))" if isAttr if (STACK.isNode or ns == 'html') and !@asset # TODO don't compile to setAttribute directly for node # mark compilation as being only for node? STACK.meta:universal = no return "{tref}.setAttribute('{key}',{val})" if STACK.tsc # how do we remove attribute then? let path = self.nameIdentifier().c if path == 'value' and @tag.@tagName in ['input','textarea','select','option','button'] # path = 'richValue' val = '/**@type {any}*/(' + val + ')' let access = "{tref}.{M(path,@name)}" return "{M(access,@name)}{op}{@autovalue ? M('true',@value) : val}" let key = self.key if key == 'tabindex' key = 'tabIndex' # why delegate differently on node and web? # should rather always delegate value to '#value'? if key == 'value' and @tag.@tagName in ['input','textarea','select','option','button'] and !STACK.isNode key = 'richValue' if ns == 'css' "{tref}.css$('{key}',{val})" elif ns == 'bind' let path = PATHIFY(value) if path isa Variable let getter = "function()\{ return {val} \}" let setter = "function(v$)\{ {val} = v$ \}" bval = "\{get:{getter},set:{setter}\}" elif path isa Array bval = "[{val[0].c(o)},{val[1].c(o)}]" "{tref}.bind$('{key}',{bval})" elif key.indexOf('--') == 0 let pars = ["'{key}'",val] let u = option(:unit) let k = StyleTheme.propAbbr(option(:propname)) if u or k pars.push(u ? STR(u) : NULL) pars.push(STR(k)) if k STACK.use('styles') let term = option(:styleterm) if term and term:param while pars:length < 4 pars.push(NULL) pars.push(term:param) # what if this is also a placeholder? # console.log term:param "{tref}.css$var({AST.cary(pars,as: 'js').join(',')})" elif key.indexOf("aria-") == 0 or (@tag and @tag.isSVG) or key == 'for' or TAG_GLOBAL_ATTRIBUTES[key] if ns "{tref}.setns$('{ns}','{key}',{val})" else "{tref}.set$('{key}',{val})" elif key.indexOf("data-") == 0 # "set('{key}',{val})" "{tref}.setAttribute('{key}',{val})" # "dataset.{key.slice(5)}{op}{val}" else OP('.',LIT(tref),key).c + "{op}{val}" export class TagStyleAttr < TagAttr export class TagAttrValue < TagPart def isPrimitive value.isPrimitive def value name def js value.c def toRaw if value isa Str return value.raw return null export class TagHandlerSpecialArg < ValueNode def isPrimitive yes def c "'~${value}'" export class TagModifiers < ListNode def isStatic # the check should be for the params, no? @nodes.every(do |item| let val = item isa Parens ? item.value : item val isa Func ? !val.nonlocals : val.isPrimitive ) def visit var keys = {FUNC: 0} for node,i in nodes let key = String(node.name) if keys[key] node.name = key + '~' + (keys[key]++) else keys[key] = 1 self def extractDynamics return @dynamics if @dynamics @dynamics = [] for part,i in nodes when part isa TagModifier for param,k in part.params if !param.isPrimitive let ref = TagDynamicArg.new(param).set( key: KEY(part.name), index: k ) part.params.swap(param,LIT('null')) @dynamics.push(ref) return @dynamics def c if STACK.tsc return '[' + nodes.map(do $1.c()).join(',') + ']' let obj = Obj.new([]) for part in nodes let val = part.params ? Arr.new(part.params) : LIT('true') obj.add(KEY(part.name),val) return obj.c # Arr.new(@nodes).c export class TagModifier < TagPart prop params def load value if value isa IdentifierExpression return value.@single return value def isPrimitive !params or params.every do |param| param.isPrimitive def visit if @name isa TagHandlerCallback @name.traverse @name = @name.value if @name isa Func # not to be isolated for tsc let evparam = @name.params.at(0,yes,'e') let stateparam = @name.params.at(1,yes,'$') @name.traverse if @name isa IsolatedFunc @value = @name @name = STR('$_') @params = ListNode.new([@value].concat(@value.leaks or [])) # @value.traverse if @value # console.log "visit modifier {@name}" @params.traverse if @params return self def js if STACK.tsc if @name isa Func return "(" + @name.c + ")(e,\{\})" # let key = let key = quoted.slice(1,-1).split('-') let inv = no if key[0][0] == '!' inv = yes key[0] = key[0].slice(1) # if key[0] == 'options' # key[0] = '___setup' let path = key[0] if key:length > 1 if path == 'emit' or path == 'flag' or path == 'css' path = "{path}-name" else path = key.join('-') path = helpers.toValidIdentifier('α' + path) let parjs = params ? params.c : '' if params and parjs == '' if path == 'αoptions' parjs = M('',MLOC(@handlerName.endLoc + 1)) else parjs = M('',MLOC(@name.endLoc + 1)) let call = "{M(path,@name)}({parjs})" if !params or params.count == 0 call = M(call,@name) if inv let loc = MLOC(@name.startLoc - 1,@name.startLoc) return M("e.{call}===true",loc) # return "e.MODIFIERS.{call}" return "e.{call}" if params and params.count > 0 "[{quoted},{params.c}]" elif params "[{quoted}]" else quoted export class TagData < TagPart def value name def isStatic !value or value.isPrimitive def isSpecial true def isProxy proxyParts isa Array def proxyParts var val = value if val isa ArgList val = val.values[0] if val isa Parens val = val.value if val isa VarOrAccess val = val.@variable or val.value if val isa Access let left = val.left let right = val.right isa Index ? val.right.value : val.right if val isa IvarAccess left ||= val.scope__.context return [left,right] return val def js var val = value if val isa ArgList val = val.values[0] if val isa Parens val = val.value if val isa VarOrAccess val = val.@variable or val.value if val isa Access let left = val.left let right = val.right isa Index ? val.right.value : val.right if val isa IvarAccess left ||= val.scope__.context let pars = [left.c,right.c] if right isa Identifier pars[1] = "'" + pars[1] + "'" "bind$('data',[{pars.join(',')}])" else "data=({val.c})" export class TagDynamicArg < ValueNode def c value.c export class TagHandler < TagPart prop params watch: yes def paramsDidSet params @chain.push(@last = TagModifier.new('options')) @last.@handlerName = @name @last.params = params def add item, type, start, end if type == TagHandlerCallback item = item.first if item isa ArgList item = TagHandlerCallback.new(item) super(item,type) def visit super STACK.use('events') # Replace with something better for debugging if @name and CUSTOM_EVENTS[String(@name)] and STACK.isWeb STACK.use(CUSTOM_EVENTS[String(@name)]) def isStatic let valStatic = !value or value.isPrimitive or (value isa Func and !value.nonlocals) # check modifiers directly? valStatic and @chain.every(do |item| let val = item isa Parens ? item.value : item val isa Func ? !val.nonlocals : val.isPrimitive ) def modsIdentifier null def js o if STACK.tsc let out = "{tagRef}.addEventListener({quoted},(e)=>" + '{\n' for mod in modifiers out += mod.c + ';\n' out += '})' return out if @standalone let up = @tag let iref = "{up.cvar}[{osym}]" let mods = modifiers let specials = mods.extractDynamics() let visit = no let out = [] let add = do |val| out.push(val) let hvar = up.hvar add "{up.hvar} = {iref} || ({iref}={mods.c(o)})" for special in specials let k = special.option(:key) let i = special.option(:index) let path = "{OP('.',hvar,k).c}[{i}]" if k == 'options' visit = yes add "({vvar}={special.c(o)},{vvar}==={path} || ({path}={vvar},{dvar}|={F.DIFF_MODIFIERS}|{F.DIFF_INLINE}))" else add "{path}={special.c(o)}" add "{up.bvar} || {up.ref}.on$({quoted},{hvar.c},{scope__.context.c})" if visit add "{up.dvar}&{F.DIFF_INLINE} && ({up.dvar}^={F.DIFF_INLINE},{hvar}[{gsym('#visit')}]?.())" return "(" + out.join(",\n") + ")" return "{tagRef}.on$({quoted},{modifiers.c},{scope__.context.c})" # swallow might be better name def consume node if node isa TagLike @tag = node @standalone = yes return self # return node.register(self) export class TagHandlerCallback < ValueNode def visit let val = value if val isa Parens val = val.value if val isa Func val = val.body # If the value is just a variable we can add it directly? if val isa Access or val isa VarOrAccess # let e = Token.new('IDENTIFIER','e') let target = val val = CALL(val,[LIT('e')]) val.@args.@startLoc = target.endLoc val.@args.@endLoc = target.endLoc # console.log 'is stack tsc?' # TODO don't generate this until visiting the taghandler # if this is a plain access it should be enough to set a reference # to the function once? value = (STACK.tsc ? Func : IsolatedFunc).new([],[val],null,{}) if value isa Func let evparam = value.params.at(0,yes,'e') let stateparam = value.params.at(1,yes,'$$') value.traverse return export class TagBody < ListNode def add item, o if item isa InterpolatedString item = item.toArray if item:length == 1 item = TagTextContent.new(item[0]) super(item,o) def consume node if node isa TagLike @nodes = @nodes.map do |child| if !(child isa Meta) # and !(child isa Assign) child.consume(node) else child return self super class TagLike < Node def initialize o = {} @options = o @flags = 0 @tagvars = {} setup(o) self def isIndexableInLoop no def sourceId @sourceId ||= STACK.sourceId + '-' + oid def body @body || @options:body def value @options:value def isReactive yes def isDetached option(:detached) def isSVG @isSVG ?= (@parent ? @parent.isSVG : false) def parentTag let el = @parent while el and !(el isa Tag) el = el.@parent return el def tagLikeParents let parents = [] let el = @parent while el isa TagLike parents.push(el) el = el.parent return parents def setup @traversed = no @consumed = [] self def osym ns = '' STACK.getSymbol(oid + ns,InternalPrefixes.SYM + (tagvarprefix or '') + ns) def root @parent ? @parent.root : self def register node if node isa If or node isa Switch flag(F.TAG_HAS_BRANCHES) node = TagSwitchFragment.new(body: node) elif node isa Loop flag(F.TAG_HAS_LOOPS) node = TagLoopFragment.new(body: node.body, value: node) elif node isa Tag flag(F.TAG_HAS_DYNAMIC_CHILDREN) if node.isSlot elif node isa Op node = node.opToIfTree if node isa If flag(F.TAG_HAS_BRANCHES) node = TagSwitchFragment.new(body: node) else flag(F.TAG_HAS_DYNAMIC_CHILDREN) node = TagContent.new(value: node) elif node isa StyleRuleSet yes else flag(F.TAG_HAS_DYNAMIC_CHILDREN) unless node isa Str node = TagContent.new(value: node) @consumed.push(node) # why consume if node isa String? node.@consumedBy = self node.@parent = self return node def flag key @flags |= key def type "frag" def unflag key @flags = @flags & ~key def hasFlag key @flags & key def isAbstract true def isOnlyChild isFirstChild && isLastChild def isFirstChild hasFlag(F.TAG_FIRST_CHILD) def isLastChild hasFlag(F.TAG_LAST_CHILD) def isIndexed option(:indexed) def isComponent @kind == 'component' def isSelf type isa Self or type isa This def isShadowRoot @tagName and @tagName == 'shadow-root' def isSlot @kind == 'slot' def isFragment @kind == 'fragment' def isMemoized !option(:unmemoized) def hasLoops hasFlag(F.TAG_HAS_LOOPS) def hasBranches hasFlag(F.TAG_HAS_BRANCHES) def hasDynamicChildren hasFlag(F.TAG_HAS_DYNAMIC_CHILDREN) def hasDynamicFlags hasFlag(F.TAG_HAS_DYNAMIC_FLAGS) def hasNonTagChildren hasLoops or hasBranches or hasDynamicChildren def hasDynamicDescendants return true if hasNonTagChildren for el in @consumed if el isa Tag return true if el.hasDynamicDescendants return false def hasChildren @consumed:length > 0 def tagvar name name = InternalPrefixes[name] or name @tagvars[name] ||= scope__.closure.temporary(null,{reuse: no,alias:"{name}{tagvarprefix}"},"{name}{tagvarprefix}") def tagvarprefix "" def level @level def parent @parent ||= option(:parent) def fragment @fragment || parent def tvar @tvar or tagvar('T') def parentRef @parentRef ||= (parent ? parent.ref : "{parentCache}._") def parentCache @parentCache ||= (parent ? parent.cvar : (isMemoized ? scope__.closure.tagCache : scope__.closure.tagTempCache)) def renderContextFn "{parentCache}[{gsym('#getRenderContext')}]" def dynamicContextFn "{parentCache}[{gsym('#getDynamicContext')}]" # built variable def bvar @bvar or (@parent ? @parent.bvar : tagvar('B')) # cache variable def cvar @cvar or (@parent ? @parent.cvar : tagvar('C')) def owncvar tagvar('C') def vvar do tagvar('V') # value variable def hvar do tagvar('H') # handler variable def kvar do tagvar('K') # key variable # for tracking specific changes -- included in end # shuold maybe link it with built def dvar do tagvar('D') # value variable def ref @ref || (@cachedRef = "{parent ? parent.cvar : ''}[{osym}]") def visit stack var o = @options var scope = @tagScope = scope__ if up isa Op set(detached: yes) let prevTag = @parent = stack.@tag @level = (@parent && @parent.@level or 0) + 1 stack.@tag = null for part in @attributes part.traverse stack.@tag = self if o:key o:key.traverse visitBeforeBody(stack) if body body.traverse visitAfterBody(stack) stack.@tag = @parent unless @parent @level = 0 consumeChildren visitAfterConsumed self def visitBeforeBody self def visitAfterBody self def consumeChildren return if @consumed:length body && body.consume(self) let first = @consumed[0] let last = @consumed[@consumed:length - 1] # too many edgecases to really utilize this if !isAbstract first.flag(F.TAG_FIRST_CHILD) if first isa TagLike last.flag(F.TAG_LAST_CHILD) if last isa TagLike for item in @consumed when item isa TagLike item.@consumedBy = self item.@parent = self item.@level = (@level + 1) item.visitAfterConsumed item.consumeChildren visitAfterConsumedChildren self def visitAfterConsumedChildren self def visitAfterConsumed self def consume node if node isa TagLike return node.register(self) if node isa Variable option('assignToVar',node) return self if node isa Assign return OP(node.op,node.left,self) elif node isa VarDeclaration return OP('=',node.left,self) elif node isa Op return OP(node.op,node.left,self) elif node isa Return # console.log "return is consuming tag" option('return',yes) return self return self export class TagTextContent < ValueNode export class TagContent < TagLike def vvar parent.vvar def bvar parent.bvar # is this not the parent bvar? def ref fragment.tvar def key # @key ||= "{parent.cvar}.{oid}" @key ||= "{parent.cvar}[{osym}]" def isStatic value isa Str or value isa Num def js let value = self.value let parts = [] let isText = (value isa Str or value isa Num or value isa TagTextContent) let isStatic = self.isStatic if STACK.tsc return value.c(o) if parent isa TagSwitchFragment or (@tvar and parent isa Tag and (parent.isSlot or isDetached)) # what if it is a call? parts.push("{@tvar}={value.c(o)}") if value isa Call or value isa BangCall # mark parent to reset imba.ctx at the end let k = "{parent.cvar}[{osym('$')}]" parts.unshift("{runtime:renderContext}.context=({k} || ({k}=\{_:{fragment.tvar}\}))") parts.push("{runtime:renderContext}.context=null") elif isOnlyChild and (value isa Str or value isa Num) return "{bvar} || {ref}.text$({value.c(o)})" elif isStatic return "{bvar} || {ref}{domCall('insert')}({value.c(o)})" elif value isa TagTextContent and isOnlyChild and !(parent isa TagSwitchFragment) return "({vvar}={value.c(o)},{vvar}==={key} || {ref}.text$(String({key}={vvar})))" else parts.push("{vvar}={value.c(o)}") let inskey = "{parent.cvar}[{osym(:i)}]" # TODO rework to only introduce context with <( dynamic )> syntax (expecting tags) if value isa Call or value isa BangCall # let k = "{parent.cvar}[{osym.c}]" let k = "{parent.cvar}[{osym('$')}]" # mark parent to reset imba.ctx at the end parts.unshift("{runtime:renderContext}.context=({k} || ({k}=\{_:{fragment.tvar}\}))") parts.push("{runtime:renderContext}.context=null") if value isa TagTextContent parts.push("({vvar}==={key}&&{bvar}) || ({inskey} = {ref}{domCall('insert')}(String({key}={vvar}),{@flags},{inskey}))") else parts.push("({vvar}==={key}&&{bvar}) || ({inskey} = {ref}{domCall('insert')}({key}={vvar},{@flags},{inskey}))") return "(" + parts.join(',') + ')' export class TagFragment < TagLike export class TagSwitchFragment < TagLike def setup super @branches = [] @inserts = [] @styles = [] def getInsertVar index @inserts[index] ||= tagvar('τ' + index + 'if') # tagvar(self.oid + '$' + index) def getStyleVar index @styles[index] ||= tagvar('τ' + index + 'css') # tagvar(self.oid + '$' + index) def tvar fragment.tvar def register node let res = super if @branches let curr = @branches[@branches:length - 1] curr && curr.push(res) return res def visitAfterConsumedChildren unless @parent isa TagSwitchFragment let max = self.assignChildIndices(0,0,self) return self def assignChildIndices start,stylestart,root # use different counters for flags? let nr = start let max = start let stylenr = stylestart let stylemax = stylestart for branch,i in @branches nr = start # stylenr = stylestart for item in branch if item isa TagSwitchFragment let res = item.assignChildIndices(nr,stylenr,root) nr = res[0] stylenr = res[1] elif item isa StyleRuleSet item.@tvar = root.getStyleVar(stylenr) item.@tvar.@stylerule = item stylenr++ else item.@tvar = root.getInsertVar(nr) item.set(detached: yes) nr++ if nr > max max = nr if stylenr > stylemax stylemax = stylenr return [max,stylemax] def js o var parts = [] var top = '' let vars = @inserts.concat(@styles) if vars.len top = vars.join(' = ') + ' = null' let wasInline = o:inline if body.isExpression o:inline = yes var out = body.c(o) o:inline = wasInline return out if STACK.tsc parts.push(top) if top parts.push(out) for item,i in @inserts let key = "{cvar}[{osym(i)}]" parts.push("({key} = {tvar}{domCall('insert')}({item},0,{key}))") for item,i in @styles let flag = item.@stylerule.@name parts.push("{tvar}.flags.toggle('{flag}',!!{item})") if o:inline return parts.join(',') else return parts.join(';\n') export class TagLoopFragment < TagLike def isKeyed option(:keyed) or hasFlag(F.TAG_HAS_BRANCHES) def isIndexableInLoop yes def consumeChildren super # determine if the order of elements will ever change inside loop if hasFlag(F.TAG_HAS_BRANCHES) set(keyed: yes) elif @consumed.every(do $1 isa TagLike and $1.isIndexableInLoop) set(indexed: yes) else set(keyed: yes) def cvar @cvar || tagvar('C') def js o if stack.isExpression let fn = CALL(FN([],[self],stack.scope),[]) # the inner tag loop has to get the opdated scope as well # fn.traverse return fn.c if STACK.tsc return "{tvar} = new DocumentFragment;\n{value.c(o)}" if parent isa TagLoopFragment and parent.isKeyed set(detached: yes) if parent isa TagSwitchFragment set(detached: yes) if parent and !@consumedBy set(detached: yes) # if @tag and @childTags let iref = option(:indexed) ? (runtime:createIndexedList) : (runtime:createKeyedList) # LIT('imba.createIndexedFragment') : LIT('imba.createKeyedFragment') # should know how many inner slots this fragment has? let cache = parent.cvar let parentRef = isDetached ? LIT('null') : fragment.tvar let out = "" let refpath if parent isa TagLoopFragment if parent.isKeyed option(:key,OP('+',LIT("'{oid}$'"),parent.kvar)) out += "{hvar}={option(:key).c};\n" refpath = @ref = "{parent.cvar}[{hvar}]" else refpath = @ref = "{parent.cvar}[{parent.kvar}]" else refpath = "{cache}[{osym}]" out += "({tvar} = {refpath}) || ({refpath}={tvar}={iref}({@flags},{parentRef}));\n" @ref = "{tvar}" if isDetached out += "{tvar}[{gsym('##up')}] = {fragment.tvar};\n" out += "{kvar} = 0;\n" out += "{cvar}={tvar}.$;\n" out += value.c(o) out += ";{tvar}{domCall 'end'}({kvar});" if parent isa TagLoopFragment if parent.isKeyed out += "{parent.ref}.push({tvar},{parent.kvar}++,{hvar});" elif parent.isIndexed out += "{parent.kvar}++;" return out export class TagIndexedFragment < TagLike export class TagKeyedFragment < TagLike export class TagSlotProxy < TagLike def ref tvar def tagvarprefix oid+'S' class TagNodeClass export class Tag < TagLike prop attrmap def setup super @attributes = @options:attributes or [] @attrmap = {} @classNames = [] @className = null def isAbstract isSlot or isFragment def attrs @attributes def cssns @cssns ||= "{sourceId}".replace('-','_') def cssid @cssid ||= "{sourceId}".replace('_','-') def cssflag @cssflag ||= "{sourceId}" def tagvarprefix return isSelf ? 'SELF' : 'T' return @tagvarprefix ||= (type and type:toVarPrefix ? type.toVarPrefix : (isSelf ? 'self' : 'tag')) return '' def isStatementLike option(:iife) def isIndexableInLoop !option(:key) and !isDynamicType def traverse return self if @traversed @tid = STACK.generateId('tag') @tagDeclaration = STACK.up(TagDeclaration) let close = @options:close let body = @options:body or [] let returns = self if close and close.@value == '/>' and body.len returns = [self].concat(body.@nodes) @options:body = ArgList.new([]) super return returns def visitBeforeBody stack oid let type = @options:type type && type.traverse if STACK.hmr self.cssid if isSelf or (tagName.indexOf('-') >= 0) or isDynamicType or (type && type.isComponent) @options:custom = yes @kind = 'component' else @kind = 'element' if attrs:length == 0 && !@options:type @options:type = 'fragment' let tagName = self.tagName if tagName == 'slot' @kind = 'slot' elif tagName == 'fragment' @kind = 'fragment' if tagName == 'shadow-root' @kind = 'shadow-root' if isSelf let decl = stack.up(TagDeclaration) decl.set(self: self,sourceId: sourceId) if decl @tagName = tagName @dynamics = [] let i = 0 while i < @attributes:length let item = @attributes[i++] if item isa TagFlag and item.name isa StyleRuleSet if item.name.placeholders:length for ph in item.name.placeholders let setter = TagStyleAttr.new(ph.name) setter.@tag = self setter.value = ph.runtimeValue setter.set( propname: ph.@propname unit: ph.option('unit') styleterm: ph ) @attributes.splice(i++,0,setter) setter.traverse @attributes = @attributes.filter do |item| if item isa TagFlag and item.isStatic @classNames.push(item) return false unless STACK.tsc if item == @attrmap:$key item.warn("$key= is deprecated, use key=", loc: item.@name) self.set(key: item.value) return false if item == @attrmap:key self.set(key: item.value) return false if !item.isStatic @dynamics.push(item) return true if @parent if @attrmap:route or isDynamicType @parent.set(shouldEnd: yes, ownCache: yes) if isSlot # @tvar = tagvar('t'+oid) let name = @attrmap:name ? @attrmap:name.value : '__' name = name.raw if name isa Str set(name: name) @attributes = [] @scope = TagBodyScope.new(self) @scope.visit super def register node node = super(node) if node isa TagLike and (isComponent and !isSelf) let slotKey = node isa Tag ? node.@attrmap:slot : null let name = '__' if slotKey if slotKey.value isa Str name = slotKey.value.raw # let name = slotKey ? slotKey.value .toRaw : '__' let slot = getSlot(name) node.@fragment = slot return node def visitAfterBody stack self def visitAfterConsumed if isSVG @kind = 'svg' if @options:reference let method = stack.up(MethodDeclaration) let tagdef = stack.up(TagDeclaration) let err if @options:key err = "Named element cannot be keyed at the same time" if tagdef and method and String(method.name) == 'render' for el in tagLikeParents if el isa TagLoopFragment err = "Named tags not allowed inside loops" if el isa Tag and el.isDynamicType err = "Named tags not allowed inside dynamic parent" unless err tagdef.addElementReference(@options:reference,self) else err = "Named tags are only allowed inside render method" if err # FIXME should actually classify as error warn(err, loc: @options:reference) # FIXME Slots are not allowed inside loops self def visitAfterConsumedChildren if isSlot and @consumed:length > 1 set(markWhenBuilt: yes, reactive: yes) return def hasBlockScopedVariables Object.keys(@scope.varmap):length > 0 def getSlot name @slots ||= {} @slots[name] ||= TagSlotProxy.new(parent: self, name: name) def addPart part, type, tok let attrs = @attributes let curr = attrs.CURRENT let next = curr if type == TagId set(id: part) if type == TagArgList if attrs:length == 0 let typ = option(:type) typ = null if typ.@token == 'div' set(dynamic: yes) let op = part.nodes[0] if typ op = CALL(typ.toFunctionalType,part.nodes) set(type: op, functional: op) return self if type == TagSep next = null elif type == TagAttrValue if part isa Parens part = part.value if curr isa TagFlag curr.condition = part flag(F.TAG_HAS_DYNAMIC_FLAGS) curr.set(op: tok) elif curr isa TagHandler if part # if part isa Access or part isa VarOrAccess # # let e = Token.new('IDENTIFIER','e') # part = CALL(part,[LIT('e')]) # console.log 'is stack tsc?' # TODO don't generate this until visiting the taghandler # part = (STACK.tsc ? Func : IsolatedFunc).new([],[part],null,{}) curr.add(TagHandlerCallback.new(part),type) elif curr curr.value = part curr.set(op: tok) elif curr isa TagHandler if part isa IdentifierExpression and part.single and !part.isPrimitive # console.log 'is stack tsc?' part = (STACK.tsc ? Func : IsolatedFunc).new([],[part.single],null,{}) curr.add(part,type) elif curr isa TagAttr curr.add(part,type) else if type == TagFlag and part isa IdentifierExpression and !part.isPrimitive flag(F.TAG_HAS_DYNAMIC_FLAGS) if part isa type part.@tag = self else part = type.new(part,self) attrs.push(next = part) if next isa TagAttr and next.name.isPrimitive let name = String(next.name.toRaw) if name.match(/^bind(?=\:|$)/) and isFunctional next.@name.error "bind not supported for functional fragments" if name == 'bind' (next.@name.@single or next.@name).@value = 'bind:data' name = 'bind:data' @attrmap[name] = next if next != curr attrs.CURRENT = next self def type @options:type || (@attributes:length == 0 ? :fragment : :div) def tagName @tagName || String(@options:type) def isDynamicType type isa ExpressionNode or @options:dynamic def isFunctional !!@options:functional def isSVG @isSVG ?= ((type isa TagTypeIdentifier and type.isSVG) or (@parent and @parent.isSVG)) def isAsset @isAsset or no def create_ if isFragment or isSlot runtime:createLiveFragment # LIT('imba.createLiveFragment') elif isAsset runtime:createAssetElement elif isSVG runtime:createSVGElement # LIT('imba.createSVGElement') elif isDynamicType runtime:createDynamic elif isComponent runtime:createComponent else runtime:createElement def isReactive option(:reactive) or (@parent ? @parent.isReactive : !(scope__ isa RootScope)) def isDetached option(:detached) def hasDynamicParts if @dynamics:length == 0 and !hasDynamicFlags and !(type isa ExpressionNode) let nodes = body ? body.values : [] if nodes.every(|v| v isa Str or (v isa Tag and !v.isDynamicType)) if !hasNonTagChildren and !isSlot and !option(:dynamic) hasDynamicParts = no return yes def js o var stack = STACK var isExpression = stack.isExpression var head = [] var out = [] var foot = [] var add = do |val| if val isa Variable val = val.toString out.push(val) var parent = self.parent var fragment = self.fragment var component = @tagDeclaration let oscope = @tagDeclaration ? @tagDeclaration.scope : null let typ = isSelf ? "self" : (isFragment ? "'fragment'" : (type:isClass and type.isClass ? type.toTypeArgument : "'" + type.@value + "'")) if type.@value == 'global' or type.@value == 'teleport' # console.warn "global will be deprecated in favor of teleport in a future version. Please use teleport instead" unless type.@value == 'teleport' typ = "'i-{type.@value}'" STACK.use('dom_teleport') if parent and !@consumedBy set(detached: yes) var parentIsInlined = o:inline var isSVG = self.isSVG var isReactive = self.isReactive var canInline = no var hasDynamicParts = yes var useRoutes = @attrmap:route or @attrmap:routeTo or @attrmap['route-to'] var shouldEnd = isComponent or useRoutes or option(:shouldEnd) if useRoutes stack.use('router') var dynamicKey = null var ownCache = option(:ownCache) or no if @asset typ = @assetRef.c # typ = "'{@assetName}'" var slotPath = "" if isSlot if root.isSelf slotPath = OP('.',OP(".",root.tvar,STR("__slots")),STR(option(:name))).c else let fn = OP(".",root.tvar,gsym('#registerFunctionalSlot')).c slotPath = "{fn}({STR(option(:name)).c})" if stack.tsc # if tag is expression we want to suppress certain warnings if type isa TagTypeIdentifier and !isSelf if type.isAsset add "{tvar} = new {M("SVGSVGElement",type)}" elif type.isClass add "{tvar} = new {M(type.toClassName,type)}" else # add "{tvar} = new globalThis.{M(type.toClassName,type)}" add "{tvar} = new {M(type.toClassName,type)}" elif isSelf add "{tvar} = {type.c}" elif isDynamicType if @options:dynamic add "{tvar} = new Γany" add "{type.c}" # this is the string else add "{tvar} = new {M('Γany',type)}" add "{type.c}" # this is the string else add "{tvar} = new {M('HTMLElement',type)}" add "{type.c}" # this is the string for item in @attributes @ref = tvar if item isa TagAttr or item isa TagHandler or item isa TagFlag add item.c(o) # M("{tvar}.{item.c(o)}",item) # add M("{tvar}.{item.c(o)}",item) self let nodes = body ? body.values : [] for item in nodes add item.c if o:inline or isExpression # o:inline = wasInline add option(:return) ? "return {tvar}" : "{tvar}" let js = '(' + out.join(',\n') + ')' return js else if option(:return) add "return {tvar}" let js = out.join(";\n") if hasBlockScopedVariables js = '{' + js + '}' return js # whether this tag should set a variable indicating # whether this was built now or not # basically whether we need a reference at all? var markWhenBuilt = shouldEnd or hasDynamicFlags or attrs:length or option(:markWhenBuilt) or isDetached or isDynamicType or !!option(:key) # when it has any attributes? - but not text or var inCondition = parent && parent.option(:condition) if isDynamicType ownCache = yes if isMemoized typ = "{owncvar}.value" else typ = type.c # @cref = "{parentCache}[{osym('$2')}]" # add unique flag to this element if it has inline styles or # we're compiling for hmr. if @cssid @classNames.unshift(cssid) for closure in STACK.closures if closure.@cssns and (!isSelf or closure != oscope) @classNames.push(closure.@cssns) for par in tagLikeParents if par.@cssns @classNames.push(par.@cssns) if component and !isSelf if let cname = component.cssref(option(:reference) ? null : scope__) let orig = component.@cssns if @classNames.indexOf(orig) >= 0 @classNames.splice(@classNames.indexOf(orig),1) @classNames.push cname if option(:reference) if oscope let name = String(option(:reference)).slice(1) @classNames.push("${name}") # just add the actual ref right? if option(:key) set(detached: yes) if @classNames:length let names = [] let dynamic = no for cls,i in @classNames if cls isa TagFlag if cls.name isa MixinIdentifier names.push(cls.name.toRaw) # dynamic = yes else names.push(cls.rawClassName) elif cls isa Node dynamic = yes names.push('${' + cls.c + '}') else names.push(cls) names = names.filter do |item,i| names.indexOf(item) == i let q = dynamic ? '`' : "'" @className = q + names.join(' ') + q var params = [ typ, (fragment and !option(:detached) ? fragment.tvar : 'null'), @className or 'null', 'null' ] # if @asset # params[0] = OP('.',@asset:ref,typ).c # # Arr.new([typ,@asset:ref]).c var nodes = body ? body.values : [] if nodes:length == 1 and nodes[0] isa TagContent and nodes[0].isStatic and !isSelf and !isSlot params[3] = nodes[0].value.c nodes = [] # checking to see if a node is static enough to be inserted directly into the dom without # any references. if @dynamics:length == 0 and !hasDynamicFlags and !dynamicKey and !isDynamicType if nodes.every(|v| v isa Str or (v isa Tag and !v.isDynamicType and !v.option(:key))) if !shouldEnd and !hasNonTagChildren and !isSlot and !option(:dynamic) and !option(:reference) hasDynamicParts = no if parent isa Tag and !(up isa Op) # console.log "CAN INLINE {tagName} {parent} {up}" canInline = yes if isFragment or isSlot params = [@flags].concat(params.slice(1,2)) # .slice(1,3) if isSlot # the slot is not supposed to be inserted immediately params[1] = 'null' var ctor = M("{create_}({params.join(',')})",type) if option(:reference) # TODO need to ensure that the name is resolved outside of render # what if it is on root? let par = params[1] params[1] = 'null' ctor = M("{create_}({params.join(',')})",type) set(ctor: ctor) # ctor = OP('=',OP('.',scope__.context,option(:reference)),LIT(ctor)).c() ctor = OP('.',scope__.context,option(:reference)).c() ctor = "({tvar}={ctor},{tvar}[{gsym('##up')}]={par},{tvar})" let decl = option(:tagdeclbody) if decl and !STACK.tsc let head = decl.@head ||= [] let ref = helpers.toValidIdentifier(option(:reference).c) let gen = option(:ctor) # let sym = STACK.getSymbol # let body = "return this[{sym}] || (this[{sym}] = {gen})" let body = "let el={gen};\n\treturn (Object.defineProperty(this,'{ref}',\{value:el\}),el);" let getter = "get {ref}()" + '{\n\t' + body + '\n}' head.push( getter ) # ctor = OP('.',scope__.context,option(:reference)).c() # should also check if there is already an element like this # ctor = CALL(OP('.',LIT(ctor),'ref$'),[STR(option(:reference))]) else ctor = "{tvar}={ctor}" if option(:assign) # push it into ctor if it is not a variable assignment # otherwise we always want to assign it ctor = OP('=',option(:assign),LIT(ctor)).c() let deeplyDynamic = hasDynamicDescendants # console.log "IS INLINE? {o:inline} {STACK.isExpression} {!!@consumedBy} {!!@parent} {isDetached}" if !@consumedBy @ref = "{tvar}" if isSelf add "{tvar}=this" add "{tvar}{domCall 'open'}()" add "({bvar}={dvar}=1,{tvar}[{osym}] === 1) || ({bvar}={dvar}=0,{tvar}[{osym}]=1)" @cvar = tvar elif isReactive # let scop = scope__.closure let k = "{parentCache}[{osym}]" if isDynamicType and isMemoized if option(:key) # what if this is the only one, should be special? add "{owncvar}={dynamicContextFn}({osym},{option(:key).c})" else add "{owncvar}={renderContextFn}({osym})" ctor = "{owncvar}.cache({ctor})" add "({bvar}={dvar}=1,{tvar}={owncvar}.run({type.c})) || ({bvar}={dvar}=0,{ctor})" elif option(:key) add "{cvar}=({k}={k}||new Map())" add "({bvar}={dvar}=1,{tvar}={cvar}.get({kvar}={option(:key).c})) || ({bvar}={dvar}=0,{cvar}.set({kvar},{ctor}))" else if isMemoized add "({bvar}={dvar}=1,{tvar}={k}) || ({bvar}={dvar}=0,{tvar}={k}={ctor})" else add "({bvar}={dvar}=0,{tvar}={ctor})" add "{bvar} || ({tvar}[{gsym('##up')}]={parentRef})" # really? @cvar = tvar @ref = tvar if isExpression and !deeplyDynamic option(:inline,canInline = yes) o:inline = yes else if isExpression option(:iife,yes) o:inline = no else add "({ctor})" @cvar = tvar if isExpression and !hasDynamicParts option(:inline,canInline = yes) o:inline = yes # console.log 'can inline',tagName else option(:iife,yes) o:inline = no else # console.log tagName,'re',isReactive,'expr',isExpression,'inline',canInline,'dyn',hasDynamicParts,'oin',o:inline # if the parent was inlined but we are too complex if o:inline and !canInline # what if this is just asking to be inlined because of a ternary? option(:iife,yes) o:inline = no if isShadowRoot let key = "{cvar}[{osym}]" add "{tvar}={key} || ({key}={fragment.tvar}.attachShadow(\{mode:'open'\}))" elif isSlot and !hasChildren add("{tvar}={slotPath}") unless parent isa TagSwitchFragment let key = "{cvar}[{osym}]" add("({key} = {fragment.tvar}{domCall('insert')}({tvar},{@flags},{key}))") elif isSlot and @consumed:length == 1 # single child can act as slot? # if it is a string we dont really want to insert it at all @consumed[0].set(dynamic: yes, detached: yes) @consumed[0].@tvar = tvar @consumed[0].@parent = parent # add("{tvar}") elif parent isa TagLoopFragment @bvar = tagvar('B') let key = option(:key) if option(:key) if isDynamicType add "{owncvar}={renderContextFn}({option(:key).c})" let gets = "{owncvar}.run({type.c})" add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{owncvar}.cache({ctor}))" else let gets = "{parentCache}.get({kvar}={option(:key).c})" add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{parentCache}.set({kvar},{ctor}))" elif parent.isIndexed let memo = "{parentCache}[{parent.kvar}]" add "({bvar}={dvar}=1,{tvar}={memo}) || ({bvar}={dvar}=0,{memo}={ctor})" elif parent.isKeyed if !isDynamicType let gets = "({kvar}={renderContextFn}({osym})).get({parent.kvar})" add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{kvar}.set({parent.kvar},{ctor}))" else let gets = "({owncvar}={dynamicContextFn}({type.osym},{parent.kvar})).run({type.c})" # .get({parent.kvar}) add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{owncvar}.cache({ctor}))" @ref = "{tvar}" if true add "{bvar}||({tvar}[{gsym('##up')}]={fragment.tvar})" # dont add cvar always! # rule is just "do we need our own cache?" if @dynamics:length or (@consumed:length and nodes:length) ownCache = yes elif !isReactive add "({ctor})" elif canInline @ref = tvar @bvar = parent.bvar add "{parent.bvar} || ({ctor})" else let key = option(:key) let cref = @cref ||= "{cvar}[{osym}]" if markWhenBuilt @bvar = tagvar('B') if isDynamicType if key add "{owncvar}={dynamicContextFn}({key.osym},{key.c})" else add "{owncvar}={renderContextFn}({type.osym})" let gets = "{owncvar}.run({type.c})" add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{owncvar}.cache({ctor}))" elif key add "{owncvar}={renderContextFn}({key.osym})" let gets = "{owncvar}.run({key.c})" add "({bvar}={dvar}=1,{tvar}={gets}) || ({bvar}={dvar}=0,{owncvar}.cache({ctor}))" else let ref = "{parentCache}[{osym}]" if markWhenBuilt add "({bvar}={dvar}=1,{tvar}={ref}) || ({bvar}={dvar}=0,{ref}={ctor})" else add "({tvar}={ref}) || ({ref}={ctor})" if isDetached add "{bvar}||({tvar}[{gsym('##up')}]={fragment.tvar})" @ref = tvar if dynamicKey ownCache = yes if parent isa TagSwitchFragment ownCache = yes if ownCache @cvar = tvar # tagvar(:c) if isDynamicType add {'if': "{tvar}[{gsym('#isRichElement')}]"} # ctx:condition = if @slots for own name, slot of @slots STACK.use("slots") let fn = isDynamicType ? gsym('#getFunctionalSlot') : gsym('#getSlot') add "{slot.tvar} = {OP('.',tvar,fn).c}('{name}',{cvar})" let flagsToConcat = [] for item in @attributes if item.@chain and item.@chain:length and !(item isa TagHandler) let mods = item.modifiers let dyn = !mods.isStatic let specials = mods.extractDynamics() let modid = item.modsIdentifier let modpath = modid ? OP('.',tvar,modid).c : "{cvar}[{mods.osym}]" if dyn add "{vvar} = {modpath} || ({mods.c(o)})" for special in specials let k = special.option(:key) let i = special.option(:index) add "{OP('.',vvar,k).c}[{i}]={special.c(o)}" add "{bvar} || ({modpath}={vvar})" else add "{bvar} || ({modpath}={mods.c(o)})" if !isReactive # buggy add item.c(o) # "{tvar}.{item.c(o)}" elif item.isStatic add "{bvar} || ({item.c(o)})" else let iref = "{cvar}[{item.osym}]" if item isa TagFlag let cond = item.condition let val = item.name let cref let vref let batched = !isDynamicType if cond and !cond.isPrimitive cref = "{cvar}[{cond.osym}]" add "({vvar}=({cond.c(o)}||undefined),{vvar}==={cref}||({dvar}|={F.DIFF_FLAGS},{cref}={vvar}))" if val and !(val isa Token) and !val.isPrimitive and !(val isa MixinIdentifier) and !(val isa StyleRuleSet) vref = "{cvar}[{val.osym}]" add "({vvar}={val.c(o)},{vvar}==={vref}||({dvar}|={F.DIFF_FLAGS},{vref}={vvar}))" if batched or true if cref and vref flagsToConcat.push("({cref} ? ({vref}||'') : '')") elif cref flagsToConcat.push("({cref} ? {val.c(as:'string')} : '')") elif vref flagsToConcat.push("({vref}||'')") elif val isa MixinIdentifier flagsToConcat.push(val.c(as:'string')) else flagsToConcat.push("'{val.c(as:'substring')}'") else if cref add "{tvar}.flags.toggle({vref ? vref : val.c(as:'string')},{cref}))" else add "({bvar}||{tvar}.flags.add({vref ? vref : val.c(as:'string')})" elif item isa TagHandler let mods = item.modifiers let specials = mods.extractDynamics() let visit = no add "{hvar} = {iref} || ({iref}={mods.c(o)})" for special in specials let k = special.option(:key) let i = special.option(:index) let path = "{OP('.',hvar,k).c}[{i}]" if k == 'options' visit = yes add "({vvar}={special.c(o)},{vvar}==={path} || ({path}={vvar},{dvar}|={F.DIFF_MODIFIERS}|{F.DIFF_INLINE}))" else add "{path}={special.c(o)}" add "{bvar} || {ref}.on$({item.quoted},{hvar.c},{scope__.context.c})" if visit add "{dvar}&{F.DIFF_INLINE} && ({dvar}^={F.DIFF_INLINE},{hvar}[{gsym('#visit')}]?.())" elif item isa TagAttr and item.ns == 'bind' let rawVal = item.value let val = PATHIFY(rawVal) shouldEnd = yes if val isa Array let target = val[0] let key = val[1] let bval = "[]" let alit = target and target.isConstant # isa Literal or target isa ScopeContext let blit = key and key.isConstant # isa Literal or key isa SymbolIdentifier if alit and blit bval = "[{target.c(o)},{key.c(o)}]" elif blit bval = "[null,{key.c(o)}]" add "{vvar}={iref} || ({iref}={ref}.bind$('{item.key}',{bval}))" for part,i in val unless part and part.isConstant # isa Literal add "{vvar}[{i}]={part.c(o)}" # add "({vvar}={rawVal.c(o)},{vvar}=={ref}.value || ({iref}={ref}.bind$('{item.key}',{mods},{bval}))" elif val isa Variable let getter = "function()\{ return {val.c(o)} \}" let setter = "function(v$)\{ {val.c(o)} = v$ \}" let bval = "\{get:{getter},set:{setter}\}" add "{bvar} || {ref}.bind$('{item.key}',{bval})" else item.option(svg: true) if isSVG let val = item.value if item.valueIsStatic add "{bvar} || ({M(item.js(o),item)})" elif val isa Func add "({item.js(o)})" elif val.@variable let vc = val.c(o) item.value = LIT("{iref}={vc}") add "({vc}==={iref} || ({M item.js(o),item}))" else item.value = LIT("{iref}={vvar}") add "({vvar}={val.c(o)},{vvar}==={iref} || ({M item.js(o),item}))" if flagsToConcat:length or ((isSelf or isDynamicType) && @className) flagsToConcat.unshift(@className) if @className let cond = "{dvar}&{F.DIFF_FLAGS}" let meth = isSelf ? 'flagSelf$' : 'flag$' cond = "(!{bvar}||{cond})" if isSelf or isDynamicType if isDynamicType add "({cond} && {tvar}.flags.reconcile({osym},{flagsToConcat.join("+' '+")}))" else add "({cond} && {tvar}.{meth}({flagsToConcat.join("+' '+")}))" # When there is only one value and that value is a static string or num - include it in ctor # loop through attributes etc # add let count = nodes:length for item in nodes if item isa Str # static for sure # should this not go into a TagLike? Definitely if isReactive add "{bvar} || {tvar}{domCall('insert')}({item.c(o)})" else add "{tvar}{domCall('insert')}({item.c(o)})" elif item isa StyleRuleSet for ph in item.placeholders let item = ph.@setter # TODO - this logic should definitely move into TagAttr.c let iref = "{cvar}[{item.osym}]" let val = item.value if item.valueIsStatic add "{bvar} || ({M(item.js(o),item)})" elif val isa Func add "({item.js(o)})" elif val.@variable let vc = val.c(o) item.value = LIT("{iref}={vc}") add "({vc}==={iref} || ({M item.js(o),item}))" else item.value = LIT("{iref}={vvar}") add "({vvar}={val.c(o)},{vvar}==={iref} || ({M item.js(o),item}))" else add item.c(o) if shouldEnd if !parent and !isSelf foot.push("{bvar} || {parentCache}.sym || !{tvar}.setup || {tvar}.setup({dvar})") foot.push("{parentCache}.sym || {tvar}{domCall 'end'}({dvar})") elif isSelf foot.push("{tvar}{domCall 'close'}({dvar})") else foot.push("{bvar} || !{tvar}.setup || {tvar}.setup({dvar})") foot.push("{tvar}{domCall 'end'}({dvar})") # horrible hacks to work around the way we join the tag parts # to expressions and/or statements if isDynamicType foot.push(endif: true) if parent isa TagLoopFragment if parent.isKeyed # the last kvar argument here is not used right now foot.push "{parent.ref}.push({tvar},{parent.kvar}++,{kvar})" elif parent.isIndexed foot.push "{parent.kvar}++" elif isFragment and parent and !(parent isa TagSwitchFragment) yes elif parent and !(parent isa TagSwitchFragment) and (isComponent or dynamicKey or option(:reference)) let pref = fragment.ref let cref = @cref if dynamicKey or isDynamicType or isDetached if fragment isa TagSlotProxy foot.push "({tvar}=={cref}) || (!{cref} && {pref}{domCall('appendChild')}({cref}={tvar})) || ({pref}{domCall('replaceChild')}({tvar},{cref}),{cref}={tvar})" else foot.push "({tvar}=={cref}) || (!{cref} && ({cref}={tvar}){domCall('insertInto')}({pref})) || {cref}{domCall('replaceWith')}({cref}={tvar},{pref})" elif !isDetached foot.push "{bvar} || {pref}{domCall('appendChild')}({tvar})" if option(:fragmented) add "{runtime:renderContext}.context=null" if !@consumedBy if option(:return) or option(:iife) foot.push "return {tvar}" elif !isReactive or o:inline foot.push "{tvar}" out = out.concat(foot) if o:inline o:inline = parentIsInlined let js = '(' let last = out:length - 1 for item,i in out if item:if js += "({item:if} && (\n" # + '{\n' else js += item:endif ? '))' : item js += ',\n' unless i == last or (out[i + 1]:endif) js += ')' # let js = '(' + out.join(',\n') + ')' if isSlot and hasChildren let post = "" unless parent isa TagSwitchFragment let key = "{cvar}[{osym}]" let key_ = "{cvar}[{osym '_'}]" let key__ = "{cvar}[{osym '__'}]" let post = "{tvar}==={key__} || ({key_} = {fragment.tvar}{domCall('insert')}({key__}={tvar},{@flags},{key_}))" js = "({tvar}={slotPath}),(!{tvar} || !{tvar}.hasChildNodes() && {js}),({post})" return js o:inline = parentIsInlined let js = '' for item in out if item:if js += "if({item:if})" + '{\n' elif item:endif js += '};\n' else js += item + ';\n' # let js = out.join(";\n") if isSlot and hasChildren let post = "" unless parent isa TagSwitchFragment let key = "{cvar}[{osym}]" let key_ = "{cvar}[{osym '_'}]" let key__ = "{cvar}[{osym '__'}]" post = "{tvar}==={key__} || ({key_} = {fragment.tvar}{domCall('insert')}({key__}={tvar},{@flags},{key_}))" js = "{tvar}={slotPath};\nif(!{tvar} || !{tvar}.hasChildNodes())\{\n{js}\n\}\n{post}" if option(:iife) js = "(()=>\{{js};\})()" js = "return {js}" if option(:return) elif hasBlockScopedVariables # only if we are not in expression? js = '{' + js + '}' return js export class TagWrapper < ValueNode def visit if value isa Array value.map(|v| v.traverse) else value.traverse self def c "{scope__.imba.c}.getTagForDom({value.c(expression: yes)})" # SELECTORS export class Selector < ListNode def initialize list, options @nodes = list or [] @options = options def add part, typ push(part) self def isExpressable yes def visit for item in @nodes item.traverse unless item isa Token def query var str = "" var ary = [] for item in nodes var val = item.c if item isa Token ary.push("'" + val.replace(/\'/g,'"') + "'") else ary.push(val) return ary.join(' + ') def toString AST.cary(nodes).join('') def js o var typ = option(:type) var q = AST.c(query) var imba = scope__.imba.c if typ == '%' "{imba}.q$({q},{o.scope.context.c(explicit: yes)})" # explicit context elif typ == '%%' "{imba}.q$$({q},{o.scope.context.c(explicit: yes)})" else "{imba}.q{typ}({q})" export class SelectorPart < ValueNode # DEFER export class Await < ValueNode prop func def js o return "await {value.c}" # if option(:native) # introduce a util here, no? CALL(OP('.',Util.Promisify.new([value]),'then'),[func]).c def visit o # things are now traversed in a somewhat chaotic order. Need to tighten # Create await function - push this value up to block, take the outer value.traverse var fnscope = o.up(Func) # do |item| item isa MethodDeclaration or item isa Fun if fnscope fnscope.set(async: yes) return self ### Top-level await is supported from node 14.8.0 but only when using loading script as es module, which breaks require etc. Right now we use our old async func transformations for top-level awaits but that feels hacky. ### warn "toplevel await not allowed" var block = o.up(Block) # or up to the closest FUNCTION? var outer = o.relative(block,1) var par = o.relative(self,-1) func = AsyncFunc.new([],[]) # now we move this node up to the block func.body.nodes = block.defers(outer,self) func.scope.visit # if the outer is a var-assignment, we can simply set the params if par isa Assign par.left.traverse var lft = par.left.node # Can be a tuple as well, no? if lft isa VarReference # the param is already registered? # should not force the name already?? # beware of bugs func.params.at(0,yes,lft.variable.name) else par.right = func.params.at(0,yes) func.body.unshift(par) func.scope.context # If it is an advance tuple or something, it should be possible to # feed in the paramlist, and let the tuple handle it as if it was any # other value # CASE If this is a tuple / multiset with more than one async value # we need to think differently. # now we need to visit the function as well func.traverse # pull the outer in self export class AsyncFunc < Func def initialize params, body, name, target, options super(params,body,name,target,options) def scopetype do LambdaScope # IMPORTS export class ESMSpecifier < Node prop alias prop name def loc @alias ? @alias.loc : @name.loc def initialize name, alias @name = name @alias = alias def visit stack @declaration = stack.up(ESMDeclaration) if @declaration isa ImportDeclaration @importer = @declaration else @exporter = @declaration @cname = helpers.clearLocationMarkers(@name.c()) @key = @alias ? helpers.clearLocationMarkers(@alias.c()) : @cname if @exporter # lookup variable unless @exporter.source @variable = scope__.root.lookup(@cname) else @variable = scope__.root.register(@key, self, type: 'imported') self def js let n = helpers.toValidIdentifier(@name.c) let a = @alias and helpers.toValidIdentifier(@alias.c) if a "{n} as {a}" else "{n}" export class ImportSpecifier < ESMSpecifier export class ImportNamespaceSpecifier < ESMSpecifier export class ExportSpecifier < ESMSpecifier export class ExportAllSpecifier < ESMSpecifier export class ImportDefaultSpecifier < ESMSpecifier export class ESMSpecifierList < ListNode def js '{' + super + '}' export class ESMDeclaration < Statement prop variable prop source def initialize keyword, specifiers, source setup @keyword = keyword @specifiers = specifiers @source = source @defaults = (specifiers and specifiers.find(do $1 isa ImportDefaultSpecifier)) def isExport String(keyword) == 'export' def js let kw = M(keyword.c,keyword) if @specifiers and @source "{kw} {AST.cary(@specifiers).join(',')} from {@source.c}" elif @specifiers "{kw} {AST.cary(@specifiers).join(',')}" elif @source "{kw} {@source.c}" export class AssetReference < ValueNode def setup self def asset @value def c let out = "" let ref = value:ref.c let path = value:path if asset:kind and path.indexOf('?') == -1 path += "?{asset:kind}" if STACK.tsc if value:pathToken # value:pathToken let pathjs = M("'{path.split('?')[0]}'",value:pathToken) out = "import {pathjs}; const {ref} = /** @type\{ImbaAsset\} */(null)" else # out = "const {ref} = /** @type\{ImbaAsset\} */(\{path:'{path}'\})" # out = "const {ref} = /** @type\{ImbaAsset\} */(\{path:'{path}'\})" out = "const {ref} = /** @type\{ImbaAsset\} */(\{path:'{path}'\})" else out = "import {ref} from {M("'{path}'",value:pathToken)}" return out export class ImportDeclaration < ESMDeclaration def ownjs var src = @source and @source.c() # console.log("importing {@source:constructor} {src}") if STACK.tsc # let raw = src.slice(1,-1) let [raw,q] = @source.raw.split('?') src = M("'{raw}'",@source) if raw.match(/\.(html|svg|png|jpe?g|gif)$/) or (q and q.match(/^\w/) and q != 'external') if @specifiers and @source let out = "{M(keyword.c,keyword)} {src};\nimport {AST.cary(@specifiers).join(',')} from 'data:text/asset;';" return out if @specifiers and @source "{M(keyword.c,keyword)} {AST.cary(@specifiers).join(',')} from {src}" else "{M(keyword.c,keyword)} {src}" def js let out = ownjs return out def push next let curr = (@next or self) @up.replace curr, [curr,BR,@next = next] def visit for item in @specifiers item?.traverse() scope__.@lastImport = self @up = up return export class ImportTypeDeclaration < ESMDeclaration def js return "" unless STACK.tsc let src = @source.c if @defaults let tpl = '/** @typedef \{import(SOURCE).default\} NAME */true' tpl = tpl.replace('SOURCE',src).replace('NAME',@defaults.c) return tpl # '/** @typedef \{import("PATH")\} NAME */' else let parts = [] for item in @specifiers[0].nodes let name = item.@name.c let alias = item.@alias ? item.@alias.c : item.@name.c let part = "/** @typedef \{import({src}).{name}\} {alias} */true" parts.push(part) # let part = tpl.replace('SOURCE',src).replace('PATH',name).replace('PATH',name) return parts.join(';\n') export class ExportDeclaration < ESMDeclaration def visit for item in @specifiers item?.traverse() self def js let kw = M(keyword.c,keyword) if @specifiers and @source "{kw} {AST.cary(@specifiers).join(',')} from {@source.c}" elif @specifiers "{kw} {AST.cary(@specifiers).join(',')}" elif @source "{kw} {@source.c}" export class ExportAllDeclaration < ExportDeclaration export class ExportNamedDeclaration < ExportDeclaration export class MixinReference prop name prop scope prop options prop rule def initialize name, scope @name = name @scope = scope @options = {} export class MixinExports < Node def add name, val @mixins ||= {} @mixins[name] = val self def c "export const mixins$ = {AST.compileRaw(@mixins or {})}" export class Export < ValueNode def loc let kw = option(:keyword) kw and kw:region ? kw.region : super def consume node if node isa Return option('return',yes) return self super def visit value.set( export: (option(:keyword) or self), return: option(:return), default: option(:default) ) super def js o # p "Export {value}" # value.set export: self, return: option(:return), default: option(:default) # if value isa VarOrAccess # return "exports.{value.c} = {value.c};" let isDefault = option(:default) if value isa ListNode value.map do |item| item.set export: self # else # value.set export: self if value isa MethodDeclaration or value isa ClassDeclaration return value.c if value isa Assign and value.left isa VarReference let ek = M('export',option(:keyword)) let dk = isDefault and M('default',option(:default)) return isDefault ? "{ek} {dk} {value.c}" : "{ek} {value.c}" if isDefault let out = value.c return "export default {out}" return value.c export class Require < ValueNode def js o var val = value isa Parens ? value.value : value var out = val.c() out == 'require' ? 'require' : "require({out})" export class EnvFlag < ValueNode def initialize super @key = String(@value).slice(1,-1) def raw @raw ?= STACK.env("" + @key) def isTruthy var val = raw return !!val if val !== undefined and !(val isa Node) return undefined def loc [0,0] def c var val = raw var out = val if val !== undefined if val isa String if val.match(/^\d+(\.\d+)?$/) out = String(parseFloat(val)) else out = "'{val}'" elif val isa Node out = out.c else out = "{val}" else out = "ENV_{@key}" return M(out,@value) export class StyleNode < Node export class StyleSelector < StyleNode # all weird parts of a selector? Or do we just compile it? export class StyleRuleSet < StyleNode # selector def initialize selectors, body @placeholders = [] @selectors = selectors @body = body def isStatic true def isGlobal !!option(:global) def addPlaceholder item @placeholders.push(item) return self def placeholders @placeholders def cssid @cssid ||= "{STACK.root.sourceId}-{oid}" def visit stack, o let cmp = @tagDeclaration = stack.up(TagDeclaration) @css = {} @flag = stack.up(TagFlag) @tag = @flag and @flag.@tag let keywordName = String(option(:name) || '') if keywordName[0] == '%' # need to be safely converted to a reference? Can do that later @mixin = scope__.mixin(keywordName.slice(1)) @mixin.rule = self @mixin.options:id = cssid if option(:export) STACK.root.mixinExports.add(@mixin.name,@mixin.options) let sel = String(@selectors).trim if stack.parent isa ClassBody # Declaration in a tag declaration let owner = stack.up(2) if owner isa TagDeclaration @css:type = 'component' if !@variable @sel = sel or '&' @css:scope = cmp else throw "css not allowed in class declaration" elif stack.parent isa TagBody # css nested inside tag tree @tag = stack.up(TagLike) @sel = sel or '&' @css:type = 'scoped' @css:scope = @tag # FIX the selector based on the tag elif option(:toplevel) let inbody = stack.up(TagBody) if inbody # Inside some logical nesting @tag = stack.up(TagLike) @sel = sel or '&' @css:scope = @tag @css:ns = cssid @css:id = cssid @css:type = 'scoped' @name = cssid set(inTagTree: yes) else @css:scope = isGlobal ? null : scope__.closure @sel ||= sel elif o:rule @sel ||= @selectors?.toString.trim # console.log "inside other rule? {@sel} | {o:rule.@sel} |" @sel = "& {@sel}" if @sel.indexOf('&') == -1 elif !@name and @tag and @flag and !@flag.@condition @css:scope = @tag @name = @tag.cssid @sel = "&" elif !@name @name = cssid # (cmp ? (cmp.cssns + oid) : (sourceId + oid)) @sel = ".{@name}" @selectors?.traverse @styles = {} @body?.traverse(rule: self, styles: @styles, rootRule: (o:rule or self)) # add the placeholderes if @placeholders:length if option(:inTagTree) for ph in @placeholders let setter = TagStyleAttr.new(ph.name) setter.@tag = @tag setter.value = ph.runtimeValue setter.set( propname: ph.@propname unit: ph.option('unit') styleterm: ph ) ph.@setter = setter setter.traverse elif !@flag for ph in @placeholders console.log "{ph}" ph.warn "Only allowed inside tag tree" if o:rule and o:styles if o:styles[@sel] let base = o:styles[@sel] helpers.deepAssign(base,@styles) else o:styles[@sel] = @styles else let component = @tagDeclaration let opts = { selectors: [] ns: @css:ns id: @css:id type: @css:type scope: @css:scope component: cmp inline: !!@flag global: !!isGlobal mixins: {} apply: {} depth: @tag ? @tag.@level : 0 } @css = StyleRule.new(null,@sel,@styles,opts).toString STACK.css.add @css, opts self def toRaw "{@name}" def c if option(:toplevel) and option(:export) # console.log "EXPORT??!",@identifier,@mixin,@name return "" if @tvar let out = ["{@tvar} = '{@name}'"] let add = do out.push($1) let cvar = @tag.cvar let bvar = @tag.bvar for ph in @placeholders let item = ph.@setter # TODO - this logic should definitely move into TagAttr.c let iref = "{cvar}[{item.osym}]" let val = item.value # TODO optimize the css variable setters if true add "{M(item.js(o),item)}" elif item.valueIsStatic add "{bvar} || ({M(item.js(o),item)})" elif val isa Func add "({item.js(o)})" elif val.@variable let vc = val.c(o) item.value = LIT("{iref}={vc}") add "({vc}==={iref} || ({M item.js(o),item}))" else item.value = LIT("{iref}={vvar}") add "({vvar}={val.c(o)},{vvar}==={iref} || ({M item.js(o),item}))" # console.log out.join('\n'),STACK.isExpression let expr = STACK.isExpression return expr ? "(" + out.join(',') + ")" : out.join(";\n") # return "{@tvar} = '{@flagIf}'" if STACK.tsc and @placeholders:length let out = [] for ph in placeholders out.push ph.runtimeValue.c let expr = STACK.isExpression return expr ? "(" + out.join(',') + ")" : out.join(";\n") if option(:inClassBody) or option(:inTagTree) or option(:toplevel) return '' let out = "'{@name}'" return out # nodes # bunch of style properties and potentially nested rules export class StyleBody < ListNode def visit let items = @nodes let i = 0 let prevname for item in items when item isa StyleDeclaration if !item.@property.@name item.@property.name = prevname prevname = item.@property.@name while i < items:length let item = items[i] let res = item.traverse if res != item if res isa Array items.splice(i,1,*res) continue # has changed? if item == items[i] i++ self def toJSON values export class StyleDeclaration < StyleNode def initialize property, expr @property = property @expr = expr isa StyleExpressions ? expr : StyleExpressions.new(expr) self def clone name, params params = @expr.clone unless params if typeof params == 'string' or typeof params == 'number' params = [params] if !(params isa Array) and (!(params isa ListNode) or params isa StyleOperation) params = [params] StyleDeclaration.new(@property.clone(name),params) def visit stack, o # see if property can be expanded let theme = stack.theme let list = stack.parent let name = String(@property.name) let alias = theme.expandProperty(name) if @expr @expr.traverse( rule: o:rule rootRule: o:rootRule, decl: self, property: @property ) if alias isa Array list.replace(self,alias.map do clone($1)) return elif alias and alias != name @property = @property.clone(alias) let method = String(alias or name).replace(/-/g,'_') @expr.traverse(decl: self, property: @property) if @expr if theme[method] and !option(:plain) let res = theme[method].apply(theme,@expr.toArray) let expanded = [] if res isa Array @expr = StyleExpressions.new(res) elif res isa Object # console.log 'theme has method',method,res for own k,v of res if k.indexOf('&') >= 0 let body = StyleBody.new([]) let rule = StyleRuleSet.new(LIT(k),body) expanded.push(rule) for own k2,v2 of v # need recursive thing here body.add(clone(k2,v2)) else expanded.push clone(k,v).set(plain: (k == name) or (k == alias)) list.replace(self,expanded) return if @expr @expr.traverse(decl: self, property: @property) @expr.set(parens: no) if o:styles let key = @property.toKey let val = @expr if o:selector key = JSON.stringify([o:selector,key]) if @property.isUnit if @property.number != 1 val = LIT("calc({val.c} / {@property.number})") # if this key has already been set we need to delete it # because we rely on the key order of the object. # Should move over to using an array for this probably if o:styles[key] delete o:styles[key] o:styles[key] = val.c(property: @property) self def toCSS "{@property.c}: {AST.cary(@expr).join(' ')}" def toJSON toCSS export class StyleProperty < StyleNode prop name prop number prop unit prop kind # modifiers # values def initialize token @token = token let raw = String(@token) if raw[0] == '#' # console.log 'color property!!',raw # raw = raw.slice(1) @kind = 'color' # also split @parts = raw.replace(/(^|\b)\$/g,'--').split(/\b(?=[\^\.\@\!])/g) # .split(/[\.\@]/g) for part,i in @parts @parts[i] = part.replace(/^\.(?=[^\.])/,'@.') @name = String(@parts[0]) if let m = @name.match(/^(\d+)([a-zA-Z]+)$/) @number = parseInt(m[1]) @unit = m[2] if !@name.match(/^[\#\w\-]/) @parts.unshift(@name = null) self def name= value if let m = value.match(/^(\d+)([a-zA-Z]+)$/) @number = parseInt(m[1]) @unit = m[2] else @number = @unit = null @name = value def name @name ||= String(@parts[0]) def clone newname StyleProperty.new([newname or name].concat(modifiers).join("")) def addModifier modifier @parts.push(modifier) self def isUnit @unit def isColor @kind == 'color' def modifiers @parts.slice(1) def toJSON name + modifiers.join("§") def toString name + modifiers.join("§") def toKey let name = isUnit ? "--u_{@unit}" : (isColor ? "--c_{@name.slice(1)}" : self.name) [name].concat(modifiers).join('§') # @parts.join('.') def c toString # lookup shorthand. If shorthand represents multiple # props then we compile it to multiple props export class StylePropertyIdentifier < StyleNode def initialize name @name = name if String(name)[0] == '$' @name = "--{String(name).slice(1)}" # val[0] == '$' ? "var(--{val.slice(1)})" : val def toJSON String(@name) def toString String(@name) export class StylePropertyModifier < StyleNode def initialize name @name = name def toJSON String(@name) def toString String(@name) export class StyleExpressions < ListNode def load list if list isa Array list = list.map do $1 isa StyleExpression ? $1 : StyleExpression.new($1) [].concat(list) def c o let out = AST.cary(@nodes,o).join(', ') if option(:parens) out = "( {out} )" return out def clone StyleExpressions.new(@nodes.slice(0)) def toArray @nodes.filter(do $1 isa StyleExpression).map(do $1.toArray ) export class StyleExpression < ListNode def load list [].concat(list) def toString AST.cary(@nodes).join(' ') def toArray @nodes.slice(0) def clone StyleExpression.new(@nodes.slice(0)) def c o if o and o:as == 'js' return AST.cary(@nodes,o).join(' ') toString def toJSON toString def toArray @nodes def toIterable @nodes def addParam param,op param.@op = op last.addParam(param) return self def reclaimParams let items = filter do $1:param for item in items let param = item:param let op = param.@op add([op,param],after: item) item.@params = [] return def visit stack, o if o and o:property let name = o:property.@name if name == 'gt' or name == 'grid-template' reclaimParams super export class StyleParens < ValueNode def visit stack, o super set(calc: !stack.up(StyleParens) and !stack.up(StyleFunction)) def c o let plain = @value.c # TODO warn when option(:unit) is set if o and o:as == 'js' return plain elif option(:calc) return "calc({plain})" else return "({plain})" export class StyleOperation < ListNode def c o return AST.cary(@nodes,o).join(' ') export class StyleTerm < ValueNode def valueOf String(@value) def toString String(@value) def toRaw valueOf def toAlpha toString def visit stack, o @token = @value @property = o:property @propname = o:property and o:property.@name self:alone = (stack.up isa StyleExpression) and stack.up.values:length == 1 let resolved = stack.theme.$value(self,0,@propname) @resolvedValue = resolved unless (stack.up(StyleParens) or stack.up(StyleFunction)) self get param @params and @params[0] def kind @kind def runtimeValue value def addParam param @params ||= [] @params.push(param) return self def c o let out = (@resolvedValue and !(@resolvedValue isa Node)) ? C(@resolvedValue) : valueOf out export class StyleInterpolationExpression < StyleTerm prop name def loc [@startLoc,@endLoc] def visit stack,o super if o:rootRule o:rootRule.addPlaceholder(self) @id = "{sourceId}_{oid}" @name = "--{@id}" @runtimeValue = value # @propname = stack.theme.expandProperty def runtimeValue @runtimeValue get unit @options and String(@options:unit) or '' def c "var(--{@id})" export class StyleFunction < Node def kind 'function' def initialize value, params @name = value @params = params def visit stack, o @property = o:property @propname = o:property and o:property.@name @params.traverse if @params and @params:traverse self def toString c def c o let name = String(@name) let pars = @params.c let out = "{name}({pars})" if (@property and @property.isColor) if let res = Color.from(out) return res.toVar out = helpers.singlequote(out) if o and o:as == 'js' return out export class StyleURL < ValueNode def c let out = String(@value) return SourceMapper.strip(out) export class StyleIdentifier < StyleTerm prop color def visit stack let raw = toString if raw.match(/^([a-zA-Z]+\d+|black|white)$/) color = "{raw}" if self:param color += "/" + self:param.toAlpha super def c o if color let val = color.toString let asvar = option(:parameterize) or (@property and @property.isColor) let pre = asvar ? '/*##*/' : '/*#*/' return pre + val let val = toString if val[0] == '$' val = "var(--{val.slice(1)})" val = helpers.singlequote(val) if o and o:as == 'js' return val else super export class StyleString < StyleTerm export class StyleColor < StyleTerm def c o let raw = toRaw let name = raw.slice(1) let rich = Color.from(raw) let a = self:param ? self:param.toAlpha : 1 if @property and @property.isColor return rich.toVar return "hsla({rich.toVar},{a})" export class StyleVar < StyleTerm def c o toString var VALID_CSS_UNITS = 'cm mm Q in pc pt px em ex ch rem vw vh vmin vmax % s ms fr deg rad grad turn Hz kHz'.split(' ') export class StyleDimension < StyleTerm prop unit prop number def initialize value @value = value let m = String(value).match(/^([\-\+]?[\d\.]*)([a-zA-Z]+|%)?$/) @number = parseFloat(m[1]) @unit = m[2] or null def clone num = @number, unit = @unit let cloned = StyleDimension.new(value) cloned.@unit = unit cloned.@number = num return cloned def toString "{@number}{@unit or ''}" def toRaw @unit ? toString : @number def c o let out = (@resolvedValue and !(@resolvedValue isa Node)) ? C(@resolvedValue) : valueOf out = helpers.singlequote(out) if o and o:as == 'js' and @unit out def valueOf if unit == 'u' return number * 4 + 'px' elif unit == null return number elif unit in VALID_CSS_UNITS return String(@value) else return "calc(var(--u_{unit},1{unit}) * {@number})" # return String(@value) def toAlpha unless unit return number + '%' else return valueOf export class StyleNumber < StyleDimension # prop unit # prop number # def initialize value,unit # super # @number = parseFloat(String(value)) # @unit = null # def valueOf # if unit == 'u' # return number * 4 + 'px' # return String(@value) # def valueOf # number # UTILS export class Util < Node prop args def initialize args @args = args # this is how we deal with it now def self.extend a,b Util.Extend.new([a,b]) def self.callImba scope, meth, args CALL(OP('.',scope.imba,Identifier.new(meth)),args) def self.repeat str, times var res = '' while times > 0 if times % 2 == 1 res += str str += str times >>= 1 return res def self.keys obj var l = Const.new("Object") var r = Identifier.new("keys") CALL(OP('.',l,r),[obj]) def self.len obj, cache var r = Identifier.new("length") var node = OP('.', obj, r) node.cache(force: yes, pool: 'len') if cache return node def self.indexOf lft, rgt var node = Util.IndexOf.new([lft,rgt]) # node.cache(force: yes, type: 'iter') if cache return node def self.slice obj, a, b var slice = Identifier.new("slice") console.log "slice {a} {b}" return CALL(OP('.',obj,slice),AST.compact([a,b])) def self.iterable obj, cache return obj if STACK.tsc var node = Util.Iterable.new([obj]) node.cache(force: yes, pool: 'iter') if cache return node def self.counter start, cache # should it not rather be a variable?!? var node = Num.new(start) # make sure it really is a number node.cache(force: yes, pool: 'counter') if cache return node def self.array size, cache var node = Util.Array.new([size]) node.cache(force: yes, pool: 'list') if cache return node def name 'requireDefault$' def js scope__.root.helper(self,helper) "{name}({@args.map(|v| v.c).join(',')})" var HELPERS = { setField: '''(target,key,value,o){ Object.defineProperty(target,key,{value:value}); };''' unit: '''(value,unit){ return value + unit; };''' optNegIndex: '''(value,index){ return value ? value[value.length + index] : null };''' negIndex: '''(value,index){ return value[value.length + index] };''' extendTag: '''(el,cls){ Object.defineProperties(el,Object.getOwnPropertyDescriptors(cls.prototype)); return el; };''' inheritClass: '''(cls){ Object.getPrototypeOf(cls.prototype).constructor?.inherited?.(cls); };''' defineName: '''(cls,name){ Object.defineProperty(cls,"name",{value:name,configurable:true}); };''' initField: '''(target,key,o){ Object.defineProperty(target,key,o); };''' watcher: '''(k,w){ return { enumerable:true, set(v){var o=this[k]; (v===o)||(this[k]=v,this[w]({value:v,oldValue:o}));}, get(){ return this[k] } }; };''' decorate: '''(decorators,target,key,desc){ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };''' contains: '''(a,b){ var res = (b && b.indexOf) ? b.indexOf(a) : [].indexOf.call(a,b); return res >= 0; };''' requireDefault: '''(obj){ return obj && obj.__esModule ? obj : { default: obj }; };''' virtualSuper: '''(target){ var up = Object.getPrototypeOf(target); var supers = Object.getOwnPropertyDescriptors(target); const map = new WeakMap(); const obj = Object.defineProperties(Object.create(up), supers); const proxy = { apply: (self, key, ...params) => { return obj[key].apply(self, params) }, get: (self, key) => { return Reflect.get(obj, key, self); }, set: (self, key, value, receiver) => { return Reflect.set(obj, key, value, self);} } return function (s) { return map.get(s) || map.set(s, new Proxy(s, proxy)) && map.get(s); } };''' } export class Util.Helper < Util def name option(:name) def helper option(:helper) for own k,v of HELPERS Util[k] = do |*args| let helper = 'function ' + k + '$__' + v Util.Helper.new(args).set(name: k + '$__', helper: helper) export class Util.Extend < Util def helper ''' function extend$__(target,ext){ // @ts-ignore const descriptors = Object.getOwnPropertyDescriptors(ext); delete descriptors.constructor; // @ts-ignore Object.defineProperties(target,descriptors); return target; }; ''' def js o # When this is triggered, we need to add it to the top of file? scope__.root.helper(self,helper) "extend$__({AST.compact(AST.cary(args)).join(',')})" export class Util.IndexOf < Util def helper ''' function idx$__(a,b){ return (b && b.indexOf) ? b.indexOf(a) : [].indexOf.call(a,b); }; ''' def js o scope__.root.helper(self,helper) # When this is triggered, we need to add it to the top of file? "idx$__({args.map(|v| v.c ).join(',')})" export class Util.Promisify < Util def helper # should also check if it is a real promise ''' function promise$__(a){ if(a instanceof Array){ console.warn("await (Array) is deprecated - use await Promise.all(Array)"); return Promise.all(a); } else { return (a && a.then ? a : Promise.resolve(a)); } } ''' def js o scope__.root.helper(self,helper) "promise$__({args.map(|v| v.c).join(',')})" export class Util.Iterable < Util def helper # now we want to allow null values as well - just return as empty collection # should be the same for for own of I guess "function iter$__(a)\{ let v; return a ? ((v=a.toIterable) ? v.call(a) : a) : a; \};" def js o return args[0].c if args[0] isa Arr # or if we know for sure that it is an array scope__.root.helper(self,helper) return "iter$__({args[0].c})" export class Util.IsFunction < Util def js o "{args[0].c}" export class Util.Array < Util def js o # When this is triggered, we need to add it to the top of file? "new Array({args.map(|v| v.c)})" class Entities def initialize root @root = root @map = [] return self def add path, object @map[path] = object unless @map.indexOf(object) >= 0 @map.push(object) self def lookup path @map[path] # def register entity # var path = entity.namepath # @map[path] ||= entity # self def plain JSON.parse(JSON.stringify(@map)) def toJSON @map class RootEntities def initialize root @root = root @map = {} return self def add path, object @map[path] = object self def register entity var path = entity.namepath @map[path] ||= entity self def plain JSON.parse(JSON.stringify(@map)) def toJSON @map # SCOPES # handles local variables, self etc. Should create references to outer scopes # when needed etc. # add class for annotations / registering methods, etc? # class Interface # should move the whole context-thingie right into scope export class Scope prop level prop context prop node prop parent prop varmap prop varpool prop params prop head prop vars prop counter prop entities def p if STACK.loglevel > 0 console.log(*arguments) self def oid @oid ||= STACK.generateId('') def stack STACK def kind @kind ||= self:constructor:name.replace('Scope','').toLowerCase() def initialize node, parent @nr = STACK.incr('scopes') @head = [] @node = node @parent = parent @vars = ScopeVariables.new([]) @entities = Entities.new(self) @meta = {} @annotations = [] @closure = self @virtual = no @counter = 0 @varmap = {} @counters = {} @varpool = [] @mixins = {} @refcounter = 0 @declListeners = [] @level = (parent ? parent.@level : -1) + 1 setup def runtime root.runtime def setup @selfless = yes def incr name = 'i' var val = @counters[name] ||= 0 @counters[name]++ return val def nextShortRef AST.counterToShortRef(@refcounter++) def memovar name, init @memovars ||= {} let item = @memovars[name] unless item item = @memovars[name] = declare(item,init) # temporary(null,{reuse: yes},"{name}") return item def mixin name @mixins[name] ||= MixinReference.new(name,self) # def cssMixinFlag name def captureVariableDeclarations blk let items = [] @declListeners.push(items) blk() @declListeners.pop() return items def meta key, value if value != undefined @meta[key] = value return self @meta[key] def namepath '?' def cssid @cssid ||= "{root.sourceId}-{oid}" def cssns @cssns ||= "{root.sourceId}_{oid}" def tagCache @tagCache ||= declare('ϲτ', LIT("{runtime:getRenderContext}()"), system: yes temporary: yes alias: 'ϲτ' ) def tagTempCache @tagTempCache ||= declare('ϲττ', LIT('{}'), system: yes temporary: yes alias: 'ϲττ' ) # def context # @context ||= ScopeContext.new(self) def context # why do we need to make sure it is referenced? unless @context if selfless @context = parent.context.fromScope(self) # @context.reference(self) else @context = ScopeContext.new(self) return @context def isInExtend closure.node.option('extension') def traverse self def visit return self if @parent @parent = STACK.scope(1) # the parent scope @level = STACK.scopes:length - 1 STACK.addScope(self) root.scopes.push(self) self def wrap scope @parent = scope.@parent scope.@parent = self self # called for scopes that are not real scopes in js # must ensure that the local variables inside of the scopes do not # collide with variables in outer scopes -- rename if needed def virtualize self def root return STACK.ROOT var scope = self while scope return scope if scope isa RootScope scope = scope.parent return null def register name, decl = null, o = {} # FIXME re-registering a variable should really return the existing one # Again, here we should not really have to deal with system-generated vars # But again, it is important if !name o:system = yes if o:system return (o:varclass or SystemVariable).new(self,name,decl,o) name = AST.sym(name) # also look at outer scopes if this is not closed? var existing = @varmap.hasOwnProperty(name) && @varmap[name] if existing if decl and existing.type != 'global' decl.error('Cannot redeclare variable') # console.log 'redeclaring variable',"{existing} {decl}",existing.type # FIXME check if existing is required to be unique as well? if existing and !o:unique and existing.type != 'global' return existing let par = o:lookup && parent && parent.lookup(name) # var type = o:system ? SystemVariable : Variable var item = (o:varclass or Variable).new(self,name,decl,o) if par item.@parent = par if !o:system and (!existing or existing.type == 'global') @varmap[name] = item if STACK.state and STACK.state:variables isa Array STACK.state:variables.push(item) if @declListeners:length for l in @declListeners l.push(item) return item def annotate obj @annotations.push(obj) self # just like register, but we automatically def declare name, init = null, o = {} var variable = name isa Variable ? name : register(name,null,o) # TODO create the variabledeclaration here instead? # if this is a sysvar we need it to be renameable var dec = @vars.add(variable,init) variable.declarator ||= dec return variable def reusevar name temporary(null,{reuse: yes},name) # what are the differences here? omj # we only need a temporary thing with defaults -- that is all # change these values, no? def temporary decl, o = {}, name = null if @systemscope and @systemscope != self return @systemscope.temporary(decl,o,name) name ||= o:name o:temporary = yes if name and o:reuse and @vars["_temp_{name}"] return @vars["_temp_{name}"] if o:pool for v in @varpool if v.pool == o:pool && v.declarator == null return v.reuse(decl) var item = SystemVariable.new(self,name,decl,o) @varpool.push(item) # It should not be in the pool unless explicitly put there? @vars.push(item) # WARN variables should not go directly into a declaration-list if name and o:reuse @vars["_temp_{name}"] = item return item def lookup name @lookups ||= {} var ret = null name = AST.sym(name) if @varmap.hasOwnProperty(name) ret = @varmap[name] else ret = parent && parent.lookup(name) if ret @nonlocals ||= {} @nonlocals[name] = ret ret def requires path, name = '' root.requires(path,name) def imba # should be the same for node and js STACK.meta:universal = no @imba ||= (STACK.isNode ? LIT("(this && this[{root.symbolRef('#imba').c}] || globalThis[{root.symbolRef('#imba').c}])") : LIT('imba')) def autodeclare variable vars.add(variable) # only if it does not exist here!!! def free variable variable.free # :owner = null # @varpool.push(variable) self def selfless !!@selfless def closure @closure def finalize self def klass var scope = self while scope scope = scope.parent return scope if scope isa ClassScope return null def head [@vars,@params] def c o = {} o:expression = no # need to fix this node.body.head = head var body = node.body.c(o) def region node.body.region def loc node.loc def dump var vars = Object.keys(@varmap).map do |k| var v = @varmap[k] # unless v.@declarator isa Scope # console.log v.name, v.@declarator:constructor:name # AST.dump(v) v.references:length ? AST.dump(v) : null var desc = nr: @nr type: self:constructor:name level: (level or 0) vars: AST.compact(vars) loc: loc return desc def toJSON dump def toString "{self:constructor:name}" def closeScope self # RootScope is wrong? Rather TopScope or ProgramScope export class RootScope < Scope prop warnings prop scopes prop entities prop object prop options prop assets prop document def initialize super register('global', self, type: 'global').@c = 'globalThis' self.REQUIRE = register('require', self, type: 'global') # .@c = 'globalThis' self.IMPORT = register('import', self, type: 'global') self.MODULE = register('module', self, type: 'global') # register 'imba', self, type: 'global', varclass: GlobalReference register 'window', self, type: 'global', varclass: WindowReference self.document = register 'document', self, type: 'global', varclass: DocumentReference register 'exports', self, type: 'global' register 'console', self, type: 'global' register 'process', self, type: 'global' register 'parseInt', self, type: 'global' register 'parseFloat', self, type: 'global' register 'setTimeout', self, type: 'global' register 'setInterval', self, type: 'global' register 'setImmediate', self, type: 'global' register 'clearTimeout', self, type: 'global' register 'clearInterval', self, type: 'global' register 'clearImmediate', self, type: 'global' register 'globalThis', self, type: 'global' register 'isNaN', self, type: 'global' register 'isFinite', self, type: 'global' register '__dirname', self, type: 'global' register '__filename', self, type: 'global' register('__realname', self, type: 'global').@c = "__filename" register('__pure__', self, type: 'global', varclass: PureReference).@c = "/* @__PURE__ */" register '_', self, type: 'global' # preregister global special variables here @requires = {} @warnings = [] @scopes = [] @helpers = [] @assets = {} @selfless = yes @implicitAccessors = [] @entities = RootEntities.new(self) @object = Obj.wrap({}) @head = [@vars] @symbolRefs = {} @importProxies = {} @vars.split = yes @imba = register('imba', self, type: 'global', varclass: ImbaRuntime, path: 'imba') # @imba = register('imba', self, type: 'global', varclass: ImbaRuntime, path: 'imba') @runtime = @imba.proxy self def importProxy name, path @importProxies[name] ||= register("${name}$", self, type: 'global', varclass: ImportProxy, path: path or name) def runtime @runtime def use item unless STACK.tsc @imba.touch("use_{item}") def sourceId @sourceId ||= STACK.sourceId # @options:sourcePath and helpers.identifierForPath(@options:sourcePath) def cssns @cssns ||= "{sourceId}_" # single-file-component options def sfco @sfco ||= declare('sfc$',LIT('{/*$sfc$*/}')) def context @context ||= RootScopeContext.new(self) def globalRef @globalRef ||= LIT('globalThis') def mixinExports unless @mixinExports @head.push(@mixinExports = MixinExports.new()) return @mixinExports def registerAsset path, kind, context, pathToken let key = path + kind if @assets[key] return @assets[key] # console.log 'registering asset',!!STACK.lastImport let insertAt = STACK.lastImport or head let asset = @assets[key] = { path: path, kind: kind, external: yes, context: context pathToken: pathToken, ref: register('asset',null,system: yes) } insertAt.push(AssetReference.new(asset)) return asset def lookup name name = AST.sym(name) @varmap[name] if @varmap.hasOwnProperty(name) def visit STACK.addScope(self) self def helper typ, value # log "add helper",typ,value if @helpers.indexOf(value) == -1 @helpers.push(value) # console.log 'adding helper',value # @head.unshift(value) return self def head @head def dump var obj = { autoself: @implicitAccessors.map(|s| s.dump) } if OPTS:analysis:scopes var scopes = @scopes.map(|s| s.dump) scopes.unshift(super.dump) obj:scopes = scopes if OPTS:analysis:entities obj:entities = @entities return obj # not yet used def requires path, name if var variable = lookup(name) return variable if var variable = @requires[name] if variable.@requirePath != path throw Error.new("{name} is already defined as require('{variable.@requirePath}')") return variable var req = Require.new(Str.new("'" + path + "'")) var variable = Variable.new(self,name,null,system: yes) var dec = @vars.add(variable, req) variable.declarator ||= dec variable.@requirePath = path @requires[name] = variable return variable def imba @imba def symbolRef name name = SourceMapper.strip(name) if STACK.tsc return @symbolRefs[name] ||= Identifier.new(name.slice(1) + "_$INTERNAL$_") let map = @symbolRefs let alias = toJSIdentifier(name) map[name] ||= declare(null,LIT("Symbol.for('{name}')"), type: 'const', system: yes, alias: alias,gsym: name) def c o = {} o:expression = no let body = node.body.c(o) let sheet = STACK.css let pre = Block.new([]) pre.head = head pre.add(LIT(sheet.js(self,STACK))) let out = pre.c(o) + '\n/*body*/\n' + body if @helpers.len out = AST.cary(@helpers).join(';\n') + '\n' + out return out export class ModuleScope < Scope def setup @selfless = no def namepath @node.namepath export class ClassScope < Scope def setup @selfless = no def namepath @node.namepath # called for scopes that are not real scopes in js # must ensure that the local variables inside of the scopes do not # collide with variables in outer scopes -- rename if needed def virtualize # console.log "virtualizing ClassScope" var up = parent for own k,v of @varmap v.resolve(up,yes) # force new resolve self def prototype @prototype ||= ValueNode.new(OP('.',context,'prototype')) export class TagScope < ClassScope export class ClosureScope < Scope export class FunctionScope < Scope export class IsolatedFunctionScope < FunctionScope def lookup name @lookups ||= {} var ret = null name = AST.sym(name) if @varmap.hasOwnProperty(name) ret = @varmap[name] else ret = parent && parent.lookup(name) # only shadow variables inside the same closure? if ret and ret.closure == parent.closure @leaks ||= Map.new @nonlocals ||= {} @nonlocals[name] = ret # FIXME in the context of functional components, # self is to be considered a leaky variable # since it can change between renders let shadow = @leaks.get(ret) unless shadow @leaks.set(ret,shadow = ShadowedVariable.new(self,name,ret)) ret = shadow ret export class MethodScope < Scope def setup @selfless = no def isInExtend parent.isInExtend def visit super if STACK.tsc and isInExtend let cls = parent.closure.node if cls.@className let ref = context.reference(LIT("/** @type {cls.@className.c} */(/** @type unknown */(this))")) context.@useReference = yes ref.c self export class FieldScope < Scope def setup @selfless = no def mergeScopeInto other for own k,v of @varmap continue if k == 'self' v.resolve(other,yes) other.declare(v) if @context and @context.@reference @context.@reference = other.context.reference yes export class LambdaScope < Scope def context # why do we need to make sure it is referenced? unless @context @context = parent.context.fromScope(self) @context export class FlowScope < Scope # these have no params themselves, refer to outer scopes -- hjmm def params @parent.params if @parent def register name, decl = null, o = {} if o:type != 'let' and o:type != 'const' and (closure != self) if var found = lookup(name) if found.type == 'let' # console.log "{name} already exists as a block-variable {decl}" decl.warn "Variable already exists in block" if decl closure.register(name,decl,o) else super(name,decl,o) # FIXME should override temporary as well def autodeclare variable # need to be unique for this parent.autodeclare(variable) def closure @parent.closure # this is important? def context @context ||= parent.context def closeScope # FIXME @context.reference if @context self def temporary refnode, o = {}, name = null (@systemscope or parent).temporary(refnode,o,name) export class CatchScope < FlowScope export class WhileScope < FlowScope def autodeclare variable vars.add(variable) export class ForScope < FlowScope def autodeclare variable vars.add(variable) export class IfScope < FlowScope export class BlockScope < FlowScope def region node.region export class TagBodyScope < FlowScope # lives in scope -- really a node??? export class Variable < Node prop scope prop name prop alias prop type prop options prop initialized prop declared prop declarator prop autodeclare prop references prop export prop value prop datatype def pool null def initialize scope, name, decl, o @ref = STACK.@counter++ @c = null @scope = scope @name = name @alias = null @initialized = yes @declarator = decl @autodeclare = no @declared = o and o:declared || no @datatype = o and o:datatype @resolved = no @options = o || {} @type = o and o:type || 'var' # what about let here= @export = no @references = [] # only needed when profiling @assignments = [] self def isImported @type == 'imported' def typedAlias @typedAlias ||= Variable.new(@scope,@name + '$TYPED$',@declarator,@options) def isGlobal name @type == 'global' and (!name or @name == name) def closure @scope.closure def assignments @assignments def vartype @vartype or (@declarator and @declarator:datatype and @declarator.datatype) # Here we can collect lots of type-info about variables # and show warnings / give advice if variables are ambiguous etc def assigned val, source @assignments.push(val) if val isa Arr # just for testing really @isArray = yes else @isArray = no self def parents let parents = [] let scope = closure.parent let res = self while scope and res and parents:length < 5 console.log 'get parents!!!' if res = scope.lookup(@name) parents.unshift(res) let newscope = res.scope.parent if scope == newscope break scope = newscope return parents def resolve scope = scope, force = no return self if @resolved and !force @resolved = yes var closure = @scope.closure var item = @shadowing or scope.lookup(@name) # console.log "resolving var {@name} {scope}",scope == @closure,@virtual # if this is a let-definition inside a virtual scope we do need if @scope != closure and @type == 'let' and @virtual # or if it is a system-variable item = closure.lookup(@name) scope = closure if item == self scope.varmap[@name] = self return self elif item # possibly redefine this inside, use it only in this scope # if the item is defined in an outer scope - we reserve the if item.scope != scope && (options:let or @type == 'let') scope.varmap[@name] = self # if we allow native let we dont need to rewrite scope? return self if (!@virtual and !@shadowing) # different rules for different variables? if @options:proxy yes else var i = 0 var orig = @name # it is the closure that we should use while scope.lookup(@name) @name = "{orig}{i += 1}" scope.varmap[@name] = self closure.varmap[@name] = self return self def reference self def node self def cache self def traverse self def free ref @declarator = null self def reuse ref @declarator = ref self def proxy par, index @proxy = [par,index] self def refcount @references:length def c params if params and params:as == 'field' return "[" + c({}) + "]" return @c if @c if @typedAlias @typedAlias.c(params) # options - proxy?? if @proxy if @proxy isa Node @c = @proxy.c else @c = @proxy[0].c if @proxy[1] @c += '[' + @proxy[1].c + ']' else resolve unless @resolved var v = (alias or name) @c = typeof v == 'string' ? helpers.toValidIdentifier(v) : v.c(as: 'variable') # allow certain reserved words # should warn on others though (!!!) # if @c == 'new' # @c = '_new' # # should happen at earlier stage to # # get around naming conventions @c = "{c}$" if RESERVED_REGEX.test(@c) # @c.match(/^(default)$/) # @c = @c + '/*' + @ref + '*/' return @c def js self.c() # variables should probably inherit from node(!) def consume node return self # this should only generate the accessors - not dael with references def accessor ref var node = LocalVarAccess.new(".",null,self) # this is just wrong .. should not be a regular accessor # @references.push([ref,el]) if ref # weird temp format return node def assignment val Assign.new('=',self,val) def addReference ref if ref isa Identifier ref.references(self) if ref:region and ref.region @references.push(ref) if ref.scope__ != @scope @noproxy = yes self def autodeclare return self if @declared @autodeclare = yes scope.autodeclare(self) @declared = yes self def predeclared @declared = yes self def toString String(name) def dump typ var name = name return null if name[0].match(/[A-Z]/) return { type: type name: name refs: AST.dump(@references, typ) } def via node ValueReferenceNode.new(self,node) export class SystemVariable < Variable def pool @options:pool # weird name for this def predeclared scope.vars.remove(self) self def resolve return self if @resolved @resolved = yes let o = @options if o:gsym @name = "{o:gsym.replace(/\#/g,'$')}$" return self let sysnr = STACK.incr('sysvar') @name = "${sysnr}" return self # unless @name # adds a very random initial name # the auto-magical goes last, or at least, possibly reuse other names # "${Math.floor(Math.random * 1000)}" let o = @options var alias = o:alias or @name var typ = o:pool var names = [].concat(o:names) var alt = null var node = null @name = null let name = alias or InternalPrefixes.ANY if (/\d/).test(name[0]) name = "_{name}" if (/\d$/).test(name) name = name + InternalPrefixes.SEP let nr = STACK.incr(name) nr = '' if nr == 1 # if sysvar starts with a greek character (used for sysvars) - dont add dollar-sign if ReservedIdentifierRegex.test(name) @name = "{name}{nr}" else @name = "{name}φ{nr}" # @name = helpers.isSystemIdentifier(name) ? "{name}{nr}" : "{name}φ{nr}" # console.log "trying to set name??",name[0],name,@name return self def name resolve @name export class ShadowedVariable < Variable export class GlobalReference < Variable export class PureReference < Variable export class ZonedVariable < GlobalReference def forScope scope ZonedVariableAccess.new(self,scope) def c "{@name}" export class DocumentReference < ZonedVariable def forScope scope self def c if STACK.isNode "{runtime:get_document}()" else "globalThis.document" export class WindowReference < GlobalReference def c if STACK.isNode "{runtime:get_window}()" else "window" export class ZonedVariableAccess < Node def initialize variable, scope @variable = variable @scope = scope def c let name = @variable.@name if STACK.isNode STACK.use("{name}") "{runtime:zone}.get('{name}',{@scope.context.c})" else # what if it is redefined somewhere? "{name}" export class ImportProxy < Variable prop proxy prop path def initialize super @path = @options:path @exports = {} @touched = {} @head = LIT("import ") @head:c = self:head.bind(self) scope.@head.unshift(@head) var getter = do |t,p,r| self.access(p) @proxy_ = Proxy.new(self,{get: getter }) def proxy @proxy_ def touch key unless @touched[key] @touched[key] = access(key) self def head # for own key,value let keys = Object.keys(@exports) let touches = Object.values(@touched) let js = [] let path = self.path if path == 'imba' path = STACK.imbaPath or 'imba' let pathjs = "'{path}'" if @importAll js.push("import * as {@name} from {pathjs};") if keys:length > 0 let out = keys.map(do |a| "{a} as {@exports[a]}").join(", ") js.push "import \{{out}\} from {pathjs};" if touches:length js.push "({touches.map(do $1.c + "()").join(",")});" return js:length ? js.join('\n') : '' def access key,ctx = null if @globalName return LIT("{M(@globalName,ctx)}.{C(key)}") let raw = C(key,mark: false) @exports[raw] ||= LIT("{@name}_{raw}") def c if !@importAll @importAll = yes # console.log "import all",STACK.current # STACK.current.warn("Referencing imba directly disables efficient tree-shaking") super export class ImbaRuntime < ImportProxy def configure options if options:runtime == 'global' or STACK.tsc @globalName = 'imba' elif options:runtime self.path = options:runtime self def head return '' if STACK.tsc return super def c if !@importAll @importAll = yes # console.log "import all",STACK.current STACK.current.warn("Referencing imba directly disables efficient tree-shaking") @c = "imba" # def access key # # if @globalName # let raw = C(key,mark: false) # @exports[raw] ||= LIT("{@name}_{raw}") export class ScopeContext < Node prop scope prop value prop reference def initialize scope, value @scope = scope @value = value @reference = null self def namepath @scope.namepath # instead of all these references we should probably # just register when it is accessed / looked up from # a deeper function-scope, and when it is, we should # register the variable in scope, and then start to # use that for further references. Might clean things # up for the cases where we have yet to decide the # name of the variable etc? def reference thisValue # if we are in constructor we do want to declare it after super @reference ||= scope.lookup('self') or scope.declare("self",thisValue == undefined ? This.new : thisValue) # {@scope.@level}_ def fromScope other IndirectScopeContext.new(other,self) def isConstant yes def c return reference.c if @useReference and @reference var val = @value # || @reference (val ? val.c : "this") def cache self def proto "{self.c}.prototype" def isGlobalContext no export class IndirectScopeContext < ScopeContext def initialize scope, parent @scope = scope @parent = parent @reference = parent.reference def reference @reference # parent.reference def c reference.c def isGlobalContext @parent.isGlobalContext export class RootScopeContext < ScopeContext def reference # should be a @reference ||= scope.lookup('global') # declare("self",scope.object, type: 'global') def c o # @reference ||= scope.declare("self",scope.object, type: 'global') # return "" if o and o:explicit return "globalThis" var val = reference # @value || @reference return (val and val != this) ? val.c : "this" # should be the other way around, no? # o and o:explicit ? super : "" def isGlobalContext yes export class Super < Node prop member prop args def initialize keyword, member @keyword = keyword @member = member super def visit @method = STACK.method @up = STACK.parent if var m = STACK.method m.set(supr: {node: STACK.blockpart, block: STACK.block, real: self}) m.set(injectInitAfter: STACK.blockpart) if @method # and @method.option('inExtension') @class = STACK.up(ClassDeclaration) args.traverse if args self def startLoc @keyword and @keyword.startLoc def endLoc @keyword and @keyword.endLoc def self.callOp name, params let op = OP('.',LIT('super'),name) CALL(op,params or [LIT('...arguments')]) def c let m = @method let up = @up let sup = LIT('super') let op let top = option(:top) let virtual = m and m.option('inExtension') let args = self.args # need to know if our method is in the initial declaration if virtual and @class sup = CALL(@class.virtualSuper,[slf]) # when super is written all by itself - it means to do the default action unless (up isa Access) or (up isa Call) if m and m.isConstructor and !member if STACK.tsc and @class and !@class.superclass return args ? "[{args.c}]" : "" let target = option(:target) or LIT('super') let fallbackArgs = option(:args) or [LIT('...arguments')] return M(CALL(target,args or fallbackArgs).c,@keyword) elif member op = OP('.',sup,member) elif m op = OP('.',sup,m.name) if m.isSetter op = OP('=',op,m.params.at(0)) elif !m.isGetter args ||= [LIT('...arguments')] if args op = CALL(op,args) return op ? (M op.c(mark: false), @keyword) : '/**/' if member return OP('.',sup,member).c if up isa Call and m and !m.isConstructor return OP('.',sup,m.name).c return "super" # constants export var BR0 = Newline.new('\n') export var BR = Newline.new('\n') export var BR2 = Newline.new('\n\n') export var SELF = Self.new export var THIS = LIT('this') export var PROTO = LIT('this.prototype') # export var SUPER = Super.new export var TRUE = True.new('true') export var FALSE = False.new('false') export var UNDEFINED = Undefined.new export var NIL = Nil.new export var ARGUMENTS = ArgsReference.new('arguments') export var EMPTY = '' export var NULL = 'null' export var RESERVED = ['default','native','enum','with'] export var RESERVED_REGEX = /^(default|native|enum|with|new|char)$/