Time slots app prototype
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

13195 lines
282 KiB

# 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 <self> 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<typeof {thistype}>"
# 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<typeof {target.c}>"
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<typeof {target.c}>"
# 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)$/