//
// Copyright (c) 2023, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 03 May 2023 Matthew Giannini Creation
//
using compiler
**
** JsType
**
class JsType : JsNode
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(CompileEsPlugin plugin, TypeDef def) : super(plugin, def)
{
this.hasNatives = null != def.slots.find |n| { n.isNative && n.parent.qname == def.qname }
this.peer = findPeer(plugin, def)
}
static CType? findPeer(CompileEsPlugin plugin, CType def)
{
CType? t := def
while (t != null)
{
slot := t.slots.find |s| { s.isNative && s.parent.qname == t.qname }
if (slot != null) return slot.parent
t = t.base
}
return null
}
override TypeDef? node() { super.node }
** Does this type have any native slots directly
const Bool hasNatives
** Compiler peer type if it has one
CType? peer { private set }
** Compiler TypeDef
TypeDef def() { this.node }
** Compiler name for the type
Str name() { def.name }
** Compiler base type
CType base() { def.base }
** Facets for this type
FacetDef[] facets() { def.facets ?: FacetDef[,] }
** Mixins for this type
CType[] mixins() { def.mixins }
** Fields
FieldDef[] fields() { def.fieldDefs }
once FieldDef[] enumFields()
{
fields.findAll { it.enumDef != null }.sort |a,b| { a.enumDef.ordinal <=> b.enumDef.ordinal }
}
** Methods (excluding instanceInit)
once MethodDef[] methods() { def.methodDefs.findAll |m| { !m.isInstanceInit } }
** Get the instanceInit method if one is defined
once MethodDef? instanceInit() { def.methodDefs.find |m| { m.isInstanceInit } }
override Str toStr() { def.signature }
//////////////////////////////////////////////////////////////////////////
// Write
//////////////////////////////////////////////////////////////////////////
override Void write()
{
// class/mixin - note mixins do not extend Obj
if (def.isMixin)
js.wl("class ${name} {", loc)
else
js.wl("class ${name} extends ${qnameToJs(base)} {", loc)
js.indent
writeCtor
if (!def.isSynthetic) js.wl("typeof() { return ${name}.type\$; }", loc).nl
mixins.each |m| { copyMixin(m) }
// slots
fields.each |f| { writeField(f) }
methods.each |m| { writeMethod(m) }
js.unindent
js.wl("}")
}
private Void copyMixin(CType ref)
{
ref.slots.each |CSlot slot|
{
if (slot.parent.isObj) return
if (slot.isAbstract) return
if (slot.isStatic)
{
// copy static fields
//
// NOTE: we don't need to do static methods because the compiler
// appears to resolve those correctly in an earlier step
if (slot.isPrivate) return
if (slot.name == "static\$init") return
if (slot is CMethod) return
slotName := methodToJs(slot.name)
js.wl("static ${slotName}() { return ${qnameToJs(slot.parent)}.${slotName}(); }").nl
return
}
if (!slot.isPrivate)
{
// check if this mixin's slot was resolved by the compiler as the
// implementation for the corresponding slot on this JsType
resolved := def.slots.find { it.qname == slot.qname }
if (resolved == null) return
}
// use mixin implementation (hijack it from the parent type's prototype)
slotName := methodToJs(slot.name)
js.wl("${slotName}() { return ${qnameToJs(slot.parent)}.prototype.${slotName}.apply(this, arguments); }").nl
}
}
private Void writeCtor()
{
js.wl("constructor() {", loc)
js.indent
if (!def.isMixin) js.wl("super();")
if (peer != null) js.wl("this.peer = new ${qnameToJs(peer)}Peer(this);", loc)
js.wl("const this\$ = this;", loc)
if (instanceInit != null)
{
plugin.curMethod = instanceInit
writeBlock(instanceInit.code)
plugin.curMethod = null
}
js.unindent
js.wl("}").nl
}
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
private Void writeField(FieldDef f)
{
privName := fieldToJs(f.name)
accessName := methodToJs(f.name)
if (f.isNative) return writeNativeField(f, accessName)
if (f.isEnum) return writeEnumField(f, accessName)
if (f.isStatic) return writeStaticField(f, privName, accessName)
// write "normal" field
// write field storage
js.wl("${privName} = ${fieldDefVal(f)};", f.loc).nl
// write synthetic public API for reading/writing the field
// private getter/setter
priv := "__${accessName}(it) { if (it === undefined) return this.${privName}; else this.${privName} = it; }"
if (f.isPrivate)
{
// generate internal getter/setter for use by compiler/reflection
js.wl("// private field reflection only")
js.wl(priv, f.loc).nl
return
}
// special handling for const fields
if (f.isConst)
{
// generate public getter
js.wl("${accessName}() { return this.${privName}; }", f.loc).nl
// but always generate a synthetic getter/setter for use by the compiler/reflection
js.wl(priv, f.loc).nl
return
}
// skip fields with no public getter or setter
// TODO: I don't think this code path ever gets triggered
if ((f.getter?.isPrivate ?: true) && (f.setter?.isPrivate ?: true)) return
// use actual field name for public api
allowSet := f.setter != null && !f.setter.isPrivate
js.w("${accessName}(", f.loc)
if (allowSet) js.w("it")
js.wl(") {")
js.indent
// closure support
getterHasClosure := ClosureFinder((MethodDef?)f.getter).exists
setterHasClosure := ClosureFinder((MethodDef?)f.setter).exists
if (!allowSet)
{
plugin.curMethod = f.getter
if (getterHasClosure) js.wl("const this\$ = ${plugin.thisName};", loc)
writeBlock(f.getter->code)
plugin.curMethod = null
}
else
{
js.wl("if (it === undefined) {").indent
plugin.curMethod = f.getter
if (getterHasClosure) js.wl("const this\$ = ${plugin.thisName};", loc)
writeBlock(f.getter->code)
plugin.curMethod = null
js.unindent.wl("}")
js.wl("else {").indent
plugin.curMethod = f.setter
if (setterHasClosure) js.wl("const this\$ = ${plugin.thisName};", loc)
writeBlock(f.setter->code)
plugin.curMethod = null
js.unindent.wl("}")
}
js.unindent.wl("}").nl
}
private static Str fieldDefVal(FieldDef f)
{
defVal := "null"
fieldType := f.fieldType
if (!fieldType.isNullable)
{
switch (fieldType.signature)
{
case "sys::Bool": defVal = "false"
case "sys::Int": defVal = "0"
case "sys::Float": defVal = "sys.Float.make(0)"
case "sys::Decimal": defVal = "sys.Decimal.make(0)"
}
}
return defVal
}
private Void writeNativeField(FieldDef f, Str accessName)
{
if (f.isStatic) throw Err("TODO:FIXIT static native field")
if (f.isPrivate) throw Err("TODO:FIXIT private native field?")
js.wl("$accessName(it) {").indent
js.wl("if (it === undefined) return this.peer.${accessName}(this);")
js.wl("this.peer.${accessName}(this, it);")
js.unindent.wl("}").nl
}
private Void writeStaticField(FieldDef f, Str privName, Str accessName)
{
target := f.parent.name
js.wl("static ${privName} = undefined;", f.loc).nl
// we generate our own special version of this
if (f.parent.isEnum && accessName == "vals") return
js.wl("static ${accessName}() {").indent
fieldAccess := "${target}.${privName}"
js.wl("if (${fieldAccess} === undefined) {").indent
// call the static initializer
// if the value is still not initialized, then set it to its default value
js.wl("${target}.${curType.staticInit.name}();")
js.wl("if (${fieldAccess} === undefined) ${fieldAccess} = ${fieldDefVal(f)};")
js.unindent.wl("}")
// we can't do it this way because if a static field is initialized in an
// actual static block, then we f.init will be null, and the the static init
// block might not have initialized the field
// js.w("if (${target}.${privName} === undefined) ${target}.${privName} = ")
// if (f.init == null) js.w(fieldDefVal(f))
// else writeExpr(f.init)
// js.wl(";")
js.wl("return ${target}.${privName};")
js.unindent.wl("}").nl
}
private Void writeEnumField(FieldDef f, Str accessName)
{
ord := f.enumDef.ordinal
js.wl("static ${accessName}() { return ${qnameToJs(f.parent)}.vals().get(${ord}); }").nl
}
//////////////////////////////////////////////////////////////////////////
// Methods
//////////////////////////////////////////////////////////////////////////
private Void writeMethod(MethodDef m)
{
plugin.curMethod = m
if (curType.isEnum)
{
if (m.isStaticInit) return writeEnumStaticInit(m)
else if (m.isStatic && m.name == "fromStr") return writeEnumFromStr(m)
}
selfJs := nameToJs("\$self")
nameJs := methodToJs(m.name)
typeJs := qnameToJs(m.parentDef)
if (typeJs != qnameToJs(def)) throw Err("??? ${typeJs} ${qnameToJs(def)}")
if (m.isInstanceCtor)
{
// write static factory make method
ctorParams := CParam[SyntheticParam(selfJs, curType)].addAll(m.params)
js.wl("static ${nameJs}${methodParams(m.params)} {", m.loc)
.indent
.wl("const ${selfJs} = new ${typeJs}();")
.wl("${typeJs}.${nameJs}\$${methodParams(ctorParams)};")
.wl("return ${selfJs};")
.unindent
.wl("}").nl
// write factory make$ method
try
{
plugin.thisName = selfJs
doWriteMethod(m, "${nameJs}\$", ctorParams)
}
finally plugin.thisName = "this"
}
else if (m.isGetter || m.isSetter)
{
// getters and setters are synthetically generated when we emit
// the field (see writeField)
return
}
else doWriteMethod(m)
plugin.curMethod = null
}
private Void doWriteMethod(MethodDef m, Str methName := methodToJs(m.name), CParam[] methParams := m.params)
{
// skip abstract methods
if (m.isAbstract) return
if (m.isStatic || m.isInstanceCtor) js.w("static ")
js.wl("${methName}${methodParams(methParams)} {", m.loc)
js.indent
// default parameters
methParams.each |param|
{
if (!param.hasDefault) return
nameJs := nameToJs(param.name)
js.w("if (${nameJs} === undefined) ${nameJs} = ", toLoc(param))
JsExpr(plugin, param->def).write
js.wl(";")
}
// closure support
writeClosureSupport(m, methName, methParams)
if (m.isNative)
{
if (m.isStatic)
{
js.wl("return ${qnameToJs(peer)}Peer.${methName}${methodParams(methParams)};", m.loc)
}
else
{
pars := CParam[SyntheticParam("this", curType)].addAll(methParams)
js.wl("return this.peer.${methName}${methodParams(pars)};", m.loc)
}
}
else
{
// ctor chaining
if (m.ctorChain != null)
{
JsExpr(plugin, m.ctorChain).write
js.wl(";")
}
// method body
writeBlock(m.code)
}
js.unindent
js.wl("}").nl
}
private Void writeClosureSupport(MethodDef m, Str methName, CParam[] methParams)
{
// if the method contains closure we need to provide them access to this
hasClosure := ClosureFinder(m).exists
if (hasClosure) js.wl("const this\$ = ${plugin.thisName};")
// if the last argument is a closure and this is a "special" typed method
// then we need to set the return type on the closure
if (!JsCallExpr.typedFuncs.contains(methName)) return
param := methParams.last
if (param == null) return
ft := resolveType(param.paramType) as FuncType
if (ft == null) return
name := nameToJs(param.name)
js.wl("${name}.__returns = ((arg) => { let r = arg; if (r == null || r == sys.Void.type\$ || !(r instanceof sys.Type)) r = null; return r; })(arguments[arguments.length-1]);")
}
** An enum static$init method is used to initialize the enum vals.
** We handle that by doing it lazily so that we don't run into
** static init ordering issues.
private Void writeEnumStaticInit(MethodDef m)
{
enumName := qnameToJs(m.parent)
valsField := "${enumName}.#vals"
js.wl("static vals() {", m.loc).indent
js.wl("if (${valsField} == null) {").indent
js.wl("${valsField} = sys.List.make(${enumName}.type\$, [").indent
enumFields.each |FieldDef f, Int i| {
def := f.enumDef
js.w("${enumName}.make(${def.ordinal}, ${def.name.toCode}, ")
def.ctorArgs.each |Expr arg, Int j| {
if (j > 0) js.w(", ")
writeExpr(arg)
}
js.wl("),")
}
js.unindent.wl("]).toImmutable();")
js.unindent.wl("}")
js.wl("return ${valsField};")
js.unindent.wl("}").nl
// TODO: this feels brittle
// some enums have static initializers for other fields
// so we still need to emit the code for those. It turns
// out they appear to be all statements including and after the first if stmt:
// if (true) {...}
// so we look for those and only write those.
js.wl("static static\$init() {").indent
// force the enum vals to be loaded because the static init code
// might be attempting to reference <Enum>.#vals field directly
js.wl("const ${uniqName} = ${enumName}.vals();")
// find the first IfStmt block. It is assumed that all static init
// prior to that is for the actual enum fields. We skip those since they are
// handled special by the compiler and do the rest of the stmts
ifIdx := m.code.stmts.findIndex |item| { item is IfStmt }
if (ifIdx != null)
{
m.code.stmts[ifIdx..-1].each |stmt| {
writeStmt(stmt)
js.wl(";")
}
}
js.unindent.wl("}").nl
}
private Void writeEnumFromStr(MethodDef m)
{
typeName := qnameToJs(m.parent)
js.w("static ").w("fromStr(name\$, checked=true)", m.loc).wl(" {").indent
js.wl("return sys.Enum.doFromStr(${typeName}.type\$, ${typeName}.vals(), name\$, checked);")
js.unindent.wl("}").nl
}
}
**************************************************************************
** SyntheticParam
**************************************************************************
internal class SyntheticParam : CParam
{
new make(Str name, CType type) { this.name = name; this.type = type }
override const Str name
private CType type
override CType paramType() { return this.type; }
override const Bool hasDefault := false
}
**************************************************************************
** ClosureFinder
**************************************************************************
internal class ClosureFinder : Visitor
{
new make(Node? node) { this.node = node }
Node? node { private set }
Bool found := false
Bool exists()
{
if (node == null) return found
node->walk(this, VisitDepth.expr)
return found
}
override Expr visitExpr(Expr expr)
{
if (expr is ClosureExpr) found = true
return Visitor.super.visitExpr(expr)
}
}