From a2051d8ed6d8d2483e3a1bd0b3598c376ecdd024 Mon Sep 17 00:00:00 2001 From: Matthew Giannini Date: Mon, 14 Aug 2023 10:14:12 -0400 Subject: [PATCH] compilerEs: compiler for generating new ES JavaScript --- src/compilerEs/build.fan | 35 + src/compilerEs/fan/CompileEsPlugin.fan | 156 ++++ src/compilerEs/fan/JsWriter.fan | 130 ++++ src/compilerEs/fan/ModuleSystem.fan | 125 ++++ src/compilerEs/fan/ast/JsExpr.fan | 813 +++++++++++++++++++++ src/compilerEs/fan/ast/JsNode.fan | 210 ++++++ src/compilerEs/fan/ast/JsPod.fan | 256 +++++++ src/compilerEs/fan/ast/JsStmt.fan | 238 ++++++ src/compilerEs/fan/ast/JsType.fan | 473 ++++++++++++ src/compilerEs/fan/util/Base64VLQ.fan | 163 +++++ src/compilerEs/fan/util/JsExtToMime.fan | 31 + src/compilerEs/fan/util/JsUnitDatabase.fan | 68 ++ src/compilerEs/fan/util/SourceMap.fan | 316 ++++++++ src/compilerEs/fan/util/TzTool.fan | 211 ++++++ 14 files changed, 3225 insertions(+) create mode 100644 src/compilerEs/build.fan create mode 100644 src/compilerEs/fan/CompileEsPlugin.fan create mode 100644 src/compilerEs/fan/JsWriter.fan create mode 100644 src/compilerEs/fan/ModuleSystem.fan create mode 100644 src/compilerEs/fan/ast/JsExpr.fan create mode 100644 src/compilerEs/fan/ast/JsNode.fan create mode 100644 src/compilerEs/fan/ast/JsPod.fan create mode 100644 src/compilerEs/fan/ast/JsStmt.fan create mode 100644 src/compilerEs/fan/ast/JsType.fan create mode 100644 src/compilerEs/fan/util/Base64VLQ.fan create mode 100644 src/compilerEs/fan/util/JsExtToMime.fan create mode 100644 src/compilerEs/fan/util/JsUnitDatabase.fan create mode 100644 src/compilerEs/fan/util/SourceMap.fan create mode 100644 src/compilerEs/fan/util/TzTool.fan diff --git a/src/compilerEs/build.fan b/src/compilerEs/build.fan new file mode 100644 index 000000000..aa0c215a4 --- /dev/null +++ b/src/compilerEs/build.fan @@ -0,0 +1,35 @@ +#! /usr/bin/env fan +// +// Copyright (c) 2023, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 31 Mar 23 Matthew Giannini Creation +// + +using build + +** +** Build: compilerJsx +** +class Build : BuildPod +{ + new make() + { + podName = "compilerEs" + summary = "Fantom to ECMAScript Compiler" + meta = ["org.name": "Fantom", + "org.uri": "https://fantom.org/", + "proj.name": "Fantom Core", + "proj.uri": "https://fantom.org/", + "license.name": "Academic Free License 3.0", + "vcs.name": "Git", + "vcs.uri": "https://github.com/fantom-lang/fantom"] + depends = ["sys 1.0", "compiler 1.0",] + srcDirs = [`fan/`, + `fan/ast/`, + `fan/util/`, + ] + docSrc = true + } +} \ No newline at end of file diff --git a/src/compilerEs/fan/CompileEsPlugin.fan b/src/compilerEs/fan/CompileEsPlugin.fan new file mode 100644 index 000000000..98a371aed --- /dev/null +++ b/src/compilerEs/fan/CompileEsPlugin.fan @@ -0,0 +1,156 @@ +// +// Copyright (c) 2023, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 27 Apr 2023 Matthew Giannini Creation +// + +using compiler + +** +** Fantom source to JavaScript source compiler - this class is +** plugged into the compiler pipeline by the compiler::CompileJs step. +** +class CompileEsPlugin : CompilerStep +{ + +////////////////////////////////////////////////////////////////////////// +// Constructor +////////////////////////////////////////////////////////////////////////// + + new make(Compiler c) : super(c) + { + this.sourcemap = SourceMap(this) + this.js = JsWriter(buf.out, sourcemap) + pod.depends.each |depend| { dependOnNames[depend.name] = true } + readJsProps + } + + private StrBuf buf := StrBuf() + SourceMap sourcemap { private set } + JsWriter js { private set } + private [Str:Str] usingAs := [:] + +////////////////////////////////////////////////////////////////////////// +// Emit State +////////////////////////////////////////////////////////////////////////// + + ** The variable name that refers to "this" in the current method context + Str thisName := "this" + + ** next unique id + private Int uid := 0 + Int nextUid() { uid++; } + + [Str:Bool] dependOnNames := [:] { def = false } + +////////////////////////////////////////////////////////////////////////// +// js.props +////////////////////////////////////////////////////////////////////////// + + private Void readJsProps() + { + f := compiler.input.baseDir.plus(`js.props`) + if (!f.exists) return + f.readProps.each |val, key| + { + if (key.startsWith("using.")) + this.usingAs[key["using.".size..-1]] = val + } + } + + ** Get the alias for this pod if one was defined in js.props, otherwise + ** return the pod name. + Str podAlias(Str podName) { usingAs.get(podName, podName) } + +////////////////////////////////////////////////////////////////////////// +// Pipeline +////////////////////////////////////////////////////////////////////////// + + override Void run() + { + if (pod.name.contains("fwt")) return + +try { + JsPod(this).write +// echo(buf.toStr) + compiler.cjs = buf.toStr + compiler.esm = toEsm(compiler.cjs) + +} catch (Err e) { echo(buf.toStr); throw e } + + buf.clear + sourcemap.write(js.line, buf.out) +// echo(buf.toStr) + compiler.cjsSourceMap = buf.toStr + } + +////////////////////////////////////////////////////////////////////////// +// ESM +////////////////////////////////////////////////////////////////////////// + + ** Converts CommonJs emitted code to ESM + private Str toEsm(Str cjs) + { + buf := StrBuf() + lines := cjs.splitLines + inRequire := false + inExport := false + i := 0 + while (true) + { + line := lines[i++] + buf.add("${line}\n") + if (line.startsWith("// cjs require begin")) i = requireToImport(buf, lines, i) + else if (line.startsWith("// cjs exports begin")) + { + // we assume this is the very last thing in the file and stop once + // we convert to ESM export statement + toExports(buf, lines, i) + break + } + } + return buf.toStr + } + + private Int requireToImport(StrBuf buf, Str[] lines, Int i) + { + // this regex matches statements that require a pod in the cjs + // and creates a group for the pod name/alias (1) and the file name (2). + regex := Regex<|^const ([^_].*)? =.*__require\('(.*)?.js'\);|> + + while (true) + { + line := lines[i++] + m := regex.matcher(line) + if (m.matches) + { + pod := m.group(1) + if (pod == "fantom") { buf.addChar('\n'); continue; } + file := m.group(2) + buf.add("import * as ${pod} from './${file}.js'\n") + } + else if (line.startsWith("// cjs require end")) { buf.add("${line}\n"); break } + else buf.addChar('\n') + } + return i + } + + private Int toExports(StrBuf buf, Str[] lines, Int i) + { + // skip: const = { + line := lines[i++] + + buf.add("export {\n") + while(true) + { + line = lines[i++] + buf.add("${line}\n") + if (line == "};") break + } + while (!(line = lines[i++]).startsWith("// cjs exports end")) continue; + + return i + } +} \ No newline at end of file diff --git a/src/compilerEs/fan/JsWriter.fan b/src/compilerEs/fan/JsWriter.fan new file mode 100644 index 000000000..8bce8d3b9 --- /dev/null +++ b/src/compilerEs/fan/JsWriter.fan @@ -0,0 +1,130 @@ +// +// 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 + +** +** JsWriter +** +class JsWriter +{ + +////////////////////////////////////////////////////////////////////////// +// Construction +////////////////////////////////////////////////////////////////////////// + + new make(OutStream out, SourceMap? sourcemap := null) + { + this.out = out + this.sourcemap = sourcemap + } + + private OutStream out + private SourceMap? sourcemap := null + Int line := 0 { private set } + Int col := 0 { private set } + private Bool needIndent := false + private Int indentation := 0 + @NoDoc Bool trace := false + +////////////////////////////////////////////////////////////////////////// +// JsWriter +////////////////////////////////////////////////////////////////////////// + + ** Write and then return this. If loc is not null, the text will be + ** added to the generated source map. + JsWriter w(Obj o, Loc? loc := null, Str? name := null) + { + if (needIndent) + { + spaces := indentation * 2 + out.writeChars(Str.spaces(spaces)) + col += spaces + needIndent = false + } + str := o.toStr + if (str.containsChar('\n')) throw Err("cannot w() str with newline: ${str}") + if (loc != null) sourcemap?.add(str, Loc(loc.file, line, col), loc, name) + if (trace) Env.cur.out.print(str) + out.writeChars(str) + col += str.size + return this + } + + ** Convenience for 'w(o,loc,name).nl'. + JsWriter wl(Obj o, Loc? loc := null, Str? name := null) + { + this.w(o, loc, name).nl + } + + ** Write newline and then return this. + JsWriter nl() + { + if (trace) Env.cur.out.printLine + out.writeChar('\n') + ++line + col = 0 + needIndent = true + out.flush + return this + } + + + ** Increment the indentation + JsWriter indent() { indentation++; return this } + + ** Decrement the indentation + JsWriter unindent() + { + indentation-- + if (indentation < 0) indentation = 0 + return this + } + + JsWriter minify(InStream in, Bool close := true) + { + inBlock := false + in.readAllLines.each |line| + { + // TODO: temp hack for inlining already minified js + if (line.size > 1024) { w(line).nl; return } + + s := line + // line comments + if (s.size > 1 && (s[0] == '/' && s[1] == '/')) return +// need to check if inside str +// i := s.index("//") +// if (i != null) s = s[0.. { + const name = m.split('.')[0]; + const fan = this.fan; + if (typeof require === 'undefined') return name == "fan" ? fan : fan[name]; + try { return require(`\${m}`); } catch (e) { /* ignore */ } + } + """ + + override const Str moduleType := "cjs" + override const File moduleDir := nodeDir.plus(`node_modules/`) + override const Str ext := "js" + override This writeBeginModule(OutStream out) + { + out.printLine(CommonJs.moduleStart) + return this + } + override OutStream writeEndModule(OutStream out) + { + out.printLine("}).call(this);") + } + protected override OutStream doWriteInclude(OutStream out, Str module, Str path) + { + // we assume the module is always in the Node.js path so we ignore + // any path and just require the name of the module + out.printLine("const ${module} = __require('${path.toUri.name}');") + } + override OutStream writeExports(OutStream out, Str[] exports) + { + out.print("module.exports = {") + exports.each |export| { out.print("${export},") } + return out.printLine("};") + } +} + +************************************************************************** +** Esm +************************************************************************** + +const class Esm : ModuleSystem +{ + new make(File nodeDir) : super(nodeDir) + { + } + + override const Str moduleType := "esm" + override const File moduleDir := nodeDir.plus(`esm/`) + override const Str ext := "js" + override Void writePackageJson([Str:Obj?] json) + { + json["type"] = "module" + super.writePackageJson(json) + } + override OutStream writeExports(OutStream out, Str[] exports) + { + out.print("export {") + exports.each |export| { out.print("${export},") } + return out.printLine("};") + } + protected override OutStream doWriteInclude(OutStream out, Str module, Str path) + { + out.printLine("import * as ${module} from '${path}';") + } + +} \ No newline at end of file diff --git a/src/compilerEs/fan/ast/JsExpr.fan b/src/compilerEs/fan/ast/JsExpr.fan new file mode 100644 index 000000000..f1a83a5a8 --- /dev/null +++ b/src/compilerEs/fan/ast/JsExpr.fan @@ -0,0 +1,813 @@ +// +// Copyright (c) 2023, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 11 May 2023 Matthew Giannini Creation +// + +using compiler + +** +** JsExpr +** +class JsExpr : JsNode +{ + new make(CompileEsPlugin plugin, Expr expr) : super(plugin, expr) + { + } + + override Expr? node() { super.node } + virtual Expr expr() { this.node } + + internal Bool isLocalDefStmt := false + + override Void write() + { + switch (expr.id) + { + case ExprId.nullLiteral: writeNullLiteral(expr) + case ExprId.trueLiteral: writeBoolLiteral(expr) + case ExprId.falseLiteral: writeBoolLiteral(expr) + case ExprId.intLiteral: writeIntLiteral(expr) + case ExprId.floatLiteral: writeFloatLiteral(expr) + case ExprId.decimalLiteral: writeDecimalLiteral(expr) + case ExprId.strLiteral: writeStrLiteral(expr) + case ExprId.durationLiteral: writeDurationLiteral(expr) + case ExprId.uriLiteral: writeUriLiteral(expr) + case ExprId.typeLiteral: writeTypeLiteral(expr) + case ExprId.slotLiteral: writeSlotLiteral(expr) + case ExprId.rangeLiteral: writeRangeLiteral(expr) + case ExprId.listLiteral: writeListLiteral(expr) + case ExprId.mapLiteral: writeMapLiteral(expr) + + case ExprId.boolNot: writeUnaryExpr(expr) + case ExprId.cmpNull: writeUnaryExpr(expr) + case ExprId.cmpNotNull: writeUnaryExpr(expr) + + case ExprId.elvis: writeElvisExpr(expr) + case ExprId.assign: writeBinaryExpr(expr) + case ExprId.same: writeBinaryExpr(expr) + case ExprId.notSame: writeBinaryExpr(expr) + case ExprId.ternary: writeTernaryExpr(expr) + + case ExprId.boolOr: writeCondExpr(expr) + case ExprId.boolAnd: writeCondExpr(expr) + + case ExprId.isExpr: writeTypeCheckExpr(expr) + case ExprId.isnotExpr: writeTypeCheckExpr(expr) + case ExprId.asExpr: writeTypeCheckExpr(expr) + case ExprId.coerce: writeTypeCheckExpr(expr) + + case ExprId.call: JsCallExpr(plugin, expr).write + case ExprId.construction: JsCallExpr(plugin, expr).write + case ExprId.shortcut: JsShortcutExpr(plugin, expr).write + case ExprId.field: writeFieldExpr(expr) + case ExprId.closure: writeClosure(expr) + + case ExprId.localVar: writeLocalVarExpr(expr) + case ExprId.thisExpr: writeThisExpr + case ExprId.superExpr: writeSuperExpr(expr) + case ExprId.itExpr: writeItExpr(expr) + case ExprId.staticTarget: writeStaticTargetExpr(expr) + case ExprId.throwExpr: writeThrowExpr(expr) + + case ExprId.unknownVar: writeUnknownVarExpr(expr) + + default: + Err().trace + expr.print(AstWriter()); Env.cur.out.printLine() + throw err("Unknown ExprId: ${expr.id} ${expr.typeof}", expr.loc) + } + } + +////////////////////////////////////////////////////////////////////////// +// Literals +////////////////////////////////////////////////////////////////////////// + + private Void writeNullLiteral(LiteralExpr x) + { + js.w("null", loc) + } + + private Void writeBoolLiteral(LiteralExpr x) + { + js.w(x.val ? "true" : "false", loc) + } + + private Void writeIntLiteral(LiteralExpr x) + { + js.w(x.val, loc) + } + + private Void writeFloatLiteral(LiteralExpr x) + { + js.w("sys.Float.make(${x.val})", loc) + } + + private Void writeDecimalLiteral(LiteralExpr x) + { + js.w("sys.Decimal.make(${x.val})", loc) + } + + private Void writeStrLiteral(LiteralExpr x) + { + Str val := x.val + esc := val.toCode('\"', true)[1..-2] // remove outer quotes + js.w("\"${esc}\"", loc) + } + + private Void writeDurationLiteral(LiteralExpr x) + { + js.w("sys.Duration.fromStr(\"${x.val.toStr}\")", loc) + } + + private Void writeUriLiteral(LiteralExpr x) + { + val := x.val.toStr.toCode('\"', true) + js.w("sys.Uri.fromStr(${val})", loc) + } + + private Void writeTypeLiteral(LiteralExpr x) + { + writeType(x.val) + } + + protected Void writeType(CType t, Loc? loc := this.loc) + { + if (t.isList || t.isMap || t.isFunc) + { + js.w("sys.Type.find(\"${t.signature}\")", loc) + } + else + { + js.w("${qnameToJs(t)}.type\$", loc) + if (t.isNullable) js.w(".toNullable()", loc) + } + } + + private Void writeSlotLiteral(SlotLiteralExpr x) + { + writeType(x.parent) + js.w(".slot(\"${x.name}\")", loc) + } + + private Void writeRangeLiteral(RangeLiteralExpr x) + { + js.w("sys.Range.make(", loc) + writeExpr(x.start) + js.w(", ") + writeExpr(x.end) + if (x.exclusive) js.w(", true", loc) + js.w(")") + } + + private Void writeListLiteral(ListLiteralExpr x) + { + // inferredType := x.ctype + // explicitType := null + // if (x.explicitType != null) + // explicitType = x.explicitType + of := ((CType)(x.explicitType ?: x.ctype)).deref->v + + js.w("sys.List.make(", loc) + writeType(of) + if (x.vals.size > 0) + { + js.w(", [") + x.vals.each |v, i| + { + if (i > 0) js.w(", ") + writeExpr(v) + } + js.w("]") + } + js.w(")") + } + + private Void writeMapLiteral(MapLiteralExpr x) + { + js.w("sys.Map.fromLiteral\$([", loc) + x.keys.each |k, i| { if (i > 0) js.w(","); writeExpr(k) } + js.w("], [") + x.vals.each |v, i| { if (i > 0) js.w(","); writeExpr(v) } + js.w("]") + + t := (MapType)(x.explicitType ?: x.ctype) + js.w(", sys.Type.find(\"${t.k.signature}\")") + js.w(", sys.Type.find(\"${t.v.signature}\")") + js.w(")") + } + +////////////////////////////////////////////////////////////////////////// +// Elvis +////////////////////////////////////////////////////////////////////////// + + private Void writeElvisExpr(BinaryExpr be) + { + var := uniqName + old := plugin.thisName + plugin.thisName = "this\$" + + js.w("((this\$) => { let ${var} = ", loc) + writeExpr(be.lhs) + js.w("; if (${var} != null) return ${var}; ", loc) + if (be.rhs isnot ThrowExpr) js.w("return ", loc) + writeExpr(be.rhs) + js.w("; })(${old})", loc) + plugin.thisName = old + } + +////////////////////////////////////////////////////////////////////////// +// Ternary +////////////////////////////////////////////////////////////////////////// + + private Void writeTernaryExpr(TernaryExpr te) + { + var := uniqName + old := plugin.thisName + plugin.thisName = "this\$" + js.w("((this\$) => { ", loc) + js.w("if ("); writeExpr(te.condition); js.w(") ") + if (te.trueExpr isnot ThrowExpr) js.w("return ", loc) + writeExpr(te.trueExpr); js.w("; ") + if (te.falseExpr isnot ThrowExpr) js.w("return ", loc) + writeExpr(te.falseExpr); js.w("; ") + js.w("})(${old})", loc) + plugin.thisName = old + } + +////////////////////////////////////////////////////////////////////////// +// Unary +////////////////////////////////////////////////////////////////////////// + + private Void writeUnaryExpr(UnaryExpr x) + { + switch (x.id) + { + case ExprId.cmpNull: writeExpr(x.operand); js.w(" == null", loc) + case ExprId.cmpNotNull: writeExpr(x.operand); js.w(" != null", loc) + default: + js.w(x.opToken.symbol, loc) + if (x.operand is BinaryExpr) js.w("(") + writeExpr(x.operand) + if (x.operand is BinaryExpr) js.w(")") + } + } + +////////////////////////////////////////////////////////////////////////// +// Binary +////////////////////////////////////////////////////////////////////////// + + private Void writeBinaryExpr(BinaryExpr x) + { + symbol := x.opToken.symbol + lhs := JsExpr(plugin, x.lhs) + rhs := JsExpr(plugin, x.rhs) + leave := x.leave + isAssign := x.assignTarget != null + + if (isAssign && lhs.expr is FieldExpr) + { + fe := (FieldExpr)lhs.expr + if (leave) + { + // code like this seems to trigger this path: + // foo = bar = 1 + var := uniqName + old := plugin.thisName + plugin.thisName = "this\$" + js.w("((this\$) => { ", loc) + js.w("let ${var} = ", loc); rhs.write; js.w("; ") + + // hack: use UnknownVarExpr so we can write this synthetic var + lhs.writeSetter(JsExpr(plugin, UnknownVarExpr(loc, null, var))); js.w(";") + js.w(" return ${var}; })(${old})", loc) + + plugin.thisName = old + } + else { lhs.writeSetter(rhs) } + } + else + { + if (isAssign && !isLocalDefStmt) js.w("(") + lhs.write + js.w(" ${symbol} ", loc) + rhs.write + if (isAssign && !isLocalDefStmt) js.w(")") + } + } + + private Void writeUnknownVarExpr(UnknownVarExpr expr) + { + js.w(expr.name) + } + +////////////////////////////////////////////////////////////////////////// +// Conditions +////////////////////////////////////////////////////////////////////////// + + private Void writeCondExpr(CondExpr ce) + { + symbol := ce.opToken.symbol + operands := ce.operands + + js.w("(") + operands.each |op, i| + { + if (i > 0 && i < operands.size) js.w(" ${symbol} ", loc) + writeExpr(op) + } + js.w(")") + } + +////////////////////////////////////////////////////////////////////////// +// Type Check +////////////////////////////////////////////////////////////////////////// + + private Void writeTypeCheckExpr(TypeCheckExpr x) + { + op := x.id == ExprId.coerce ? "coerce" : x.opStr + if (op == "isnot") + { + js.w("!", loc) + op = "is" + } + js.w("sys.ObjUtil.${op}(", loc) + writeExpr(x.target) + js.w(", ") + writeType(x.check) + js.w(")") + } + +////////////////////////////////////////////////////////////////////////// +// Field +////////////////////////////////////////////////////////////////////////// + + private JsExpr? setArg := null + + Void writeSetter(JsExpr rhs) + { + try + { + this.setArg = rhs + this.write + } + finally this.setArg = null + } + + private Void writeFieldExpr(FieldExpr fe) + { + if (fe.target is SuperExpr) writeSuperField(fe) + else writeNormField(fe) + } + + private Void writeSuperField(FieldExpr fe) + { + name := nameToJs(fe.field.name) + + // TODO: do we need anything special here for setArg != null ? + // possibly if we change to getter/setter design + // if (setArg != null) throw Err("TODO: setArg = ${setArg} name=${name}") + + writeExpr(fe.target) + js.w(".${name}.call(${plugin.thisName}", loc) + if (setArg != null) + { + js.w(", ") + writeSetArg + } + js.w(")") + } + + private Void writeNormField(FieldExpr fe) + { + old := plugin.thisName + name := nameToJs(fe.field.name) + parent := fe.field.parent + useAccessor := fe.useAccessor + + // use accessor if referring to an enum +if (plugin.curMethod == null) throw Err("curMethod is null") + if (fe.field.isEnum) useAccessor = true + // use the accessor for static fields unless we are in the static initializer; + // in which case we need to access the private fields directly to initialize them. + else if (fe.field.isStatic) useAccessor = !plugin.curMethod.isStaticInit + + // force use of the accessor methods if we are accessing the + // field outside its type since we declare all fields as private + // in the generated js code + if (parent.qname != plugin.curType.qname) useAccessor = true + + writeTarget := |->| { + if (fe.target == null) js.w(qnameToJs(parent), fe.loc) + else JsExpr(plugin, fe.target).write + } + + if (fe.isSafe) + { + v := uniqName + plugin.thisName = "this\$" + js.w("((this\$) => { let ${v}=", loc) + writeTarget() + js.w("; return (${v}==null) ? null : ${v}", loc) + plugin.thisName = old + } + else + { + writeTarget() + if (name == "\$this") return // skip $this ref for closures + } + js.w(".") + if (useAccessor) + { + if (setArg==null) js.w("${name}()", fe.loc) + else + { + if (fe.field.isConst) name = "__${name}" + js.w("${name}(") + writeSetArg() + js.w(")") + } + } + else + { + js.w("${fieldJs(fe.field.name)}", fe.loc) + if (setArg != null) + { + js.w(" = ") + writeSetArg() + } + } + + if (fe.isSafe) js.w("; })(${old})", loc) + } + + private Void writeSetArg() + { + arg := this.setArg + this.setArg = null + arg.write + this.setArg = arg + } + +////////////////////////////////////////////////////////////////////////// +// Closure +////////////////////////////////////////////////////////////////////////// + + private Void writeClosure(ClosureExpr ce) + { + CType[] sigTypes := [,].addAll(ce.signature.params).add(ce.signature.ret) + isJs := sigTypes.all { !it.isForeign && checkJsSafety(it, loc) } + if (isJs) + { + js.w("${methodParams(ce.doCall.params)}", loc).wl(" => {") + js.indent + old := plugin.thisName + plugin.thisName = "this\$" + writeBlock(ce.doCall.code) + plugin.thisName = old + js.unindent + js.w("}") + } + else + { + // this closure uses non-JS types. Write a closure that documents this fact + js.wl("() => {") + js.wl(" // Cannot write closure. Signature uses non-JS types: ${ce.signature}") + js.wl(" throw sys.UnsupportedErr.make('Closure uses non-JS types: ' + ${ce.signature.toStr.toCode});") + js.w("}") + } + } + +////////////////////////////////////////////////////////////////////////// +// Throw +////////////////////////////////////////////////////////////////////////// + + private Void writeThrowExpr(ThrowExpr te) + { + js.w("throw ", loc) + writeExpr(te.exception) + } + +////////////////////////////////////////////////////////////////////////// +// This +////////////////////////////////////////////////////////////////////////// + + private Void writeThisExpr() { js.w(plugin.thisName, loc) } + +////////////////////////////////////////////////////////////////////////// +// Super +////////////////////////////////////////////////////////////////////////// + + private Void writeSuperExpr(SuperExpr se) + { + t := se.explicitType ?: se.ctype + js.w("${qnameToJs(t)}.prototype", loc) + } + +////////////////////////////////////////////////////////////////////////// +// It +////////////////////////////////////////////////////////////////////////// + + private Void writeItExpr(ItExpr ie) { js.w("it", loc) } + +////////////////////////////////////////////////////////////////////////// +// StaticTarget +////////////////////////////////////////////////////////////////////////// + + private Void writeStaticTargetExpr(StaticTargetExpr st) + { + js.w(qnameToJs(st.ctype), loc) + } + +////////////////////////////////////////////////////////////////////////// +// LocalVar +////////////////////////////////////////////////////////////////////////// + + private Void writeLocalVarExpr(LocalVarExpr x) + { + js.w(nameToJs(x.var.name), loc) + } +} + +************************************************************************** +** JsCallExpr +************************************************************************** + +internal class JsCallExpr : JsExpr +{ + new make(CompileEsPlugin plugin, CallExpr ce) : super(plugin, ce) + { + this.ce = ce + this.name = nameToJs(ce.method.name) + + if (ce.method != null) + { + this.parent = ce.method.parent + this.isCtor = ce.method.isCtor + this.isObj = ce.method.parent.qname == "sys::Obj" + this.isFunc = ce.method.parent.qname == "sys::Func" + this.isPrim = isPrimitive(ce.method.parent) + this.isStatic = ce.method.isStatic + } + + if (ce.target != null) + { + resolveType := |CType ctype->CType| { + t := ctype is TypeRef ? ctype->t : ctype + if (t is NullableType) t = t->root + return t + } + this.targetType = ce.target.ctype == null ? this.parent : ce.target.ctype + resolved := resolveType(ce.target.ctype) + funcType := c.ns.resolveType("sys::Func") + isClos = resolved.fits(funcType) + } + + // force these methods to route thru ObjUtil if not a super.xxx expr + if ((name == "equals" || name == "compare") && (ce.target isnot SuperExpr)) isObj = true + } + + CallExpr ce { private set } + Str name // js method name + Bool isObj := false // is target sys::Obj + Bool isFunc := false // is target sys::Func + Bool isPrim := false // is target a primitive type (Int,Bool,etc.) + Bool isCtor := false // is this a ctor call + Bool isStatic := false // is this a static method + Bool isClos := false // is this a Func/Closure call + CType? parent := null // method parent type + CType? targetType := null // call target type + Str? safeVar := null // var that target expr is held in for safe-nav + + override Void write() + { + // skip mock methods used to insert implicit runtime checks + if (ce.method is MockMethod) return + + // skip instance inits + if (ce.method.name.startsWith("instance\$init\$")) return + + if (ce.isSafe) + { + // wrap if safe-nav + safeVar := uniqName + old := plugin.thisName + plugin.thisName = "this\$" + js.w("((this\$) => { let ${safeVar} = ", loc) + if (ce.target == null) js.w(plugin.thisName, loc) + else writeExpr(ce.target) + js.w("; if (${safeVar} == null) return null; return ", loc) + writeCall + js.w("; })(${old})", loc) + plugin.thisName = old + } + else + { + writeCall + } + } + + protected Void writeCall() + { + if (isObj) + { + js.w("sys.ObjUtil.${name}(", loc) + if (isStatic) writeArgs + else + { + writeTarget + writeArgs(true) + } + js.w(")") + } + else if (isPrim || isFunc) + { + js.w("${qnameToJs(targetType)}.${name}(", loc) + if (isStatic) writeArgs + else + { + writeTarget + writeArgs(true) + } + js.w(")") + } + else if (ce.isCtorChain) + { + js.w("${qnameToJs(targetType)}.${name}\$(${plugin.thisName}", loc) + writeArgs(true) + js.w(")") + } + else if (ce.target is SuperExpr) + { + writeSuper + } + else + { + writeTarget + // if native closure, we invoke the func directly (don't do Func.call()) + if (isClos) js.w("(") + else js.w(".${name}(", loc) + writeArgs + js.w(")") + } + } + + protected Void writeSuper() + { + writeExpr(ce.target) + js.w(".${name}.call(this", loc) + writeArgs(true) + js.w(")") + } + + protected Void writeTarget() + { + if (isStatic || isCtor) js.w(qnameToJs(parent)) + else if (safeVar != null) js.w(safeVar) + else if (ce.target == null) js.w(plugin.thisName) + else writeExpr(ce.target) + } + + protected Void writeArgs(Bool hasFirstArg := false) + { + if (ce.isDynamic) + { + if (hasFirstArg) js.w(",") + js.w("\"${ce.name}\", sys.List.make(sys.Obj.type\$.toNullable(), [", loc) + hasFirstArg = false + } + + ce.args.each |arg, i| + { + if (hasFirstArg || i > 0) js.w(", ") + writeExpr(arg) + } + + if (ce.args.last is ClosureExpr && typedFuncs.contains(name)) + { + ClosureExpr ce := ce.args.last + js.w(", ") + writeType(ce.doCall.returnType) + } + + if (ce.isDynamic) js.w("])") + } + private static const Str[] typedFuncs := ["map", "mapNotNull", "flatMap", "groupBy"] +} + +************************************************************************** +** JsShortcutExpr +************************************************************************** + +internal class JsShortcutExpr : JsCallExpr +{ + new make(CompileEsPlugin plugin, ShortcutExpr se) : super(plugin, se) + { + this.se = se + this.isIndexedAssign = se is IndexedAssignExpr + this.isPostfixLeave = se.isPostfixLeave + + switch (se.opToken.symbol) + { + case "!=": this.name = "compareNE" + case "<": this.name = "compareLT" + case "<=": this.name = "compareLE" + case ">=": this.name = "compareGE" + case ">": this.name = "compareGT" + } + + if (se.isAssign) assignTarget = findAssignTarget(se.target) + if (isIndexedAssign) assignIndex = findIndexedAssign(se.target).args[0] + } + + ShortcutExpr se { private set } + // Bool isAssign := false // does this expr assign + Bool isIndexedAssign := false // is indexed assign + Bool isPostfixLeave := false // is postfix expr + // Bool leave := false // leave result of expr on "stack" + Bool fieldSet := false // transiently used for field sets + Expr? assignTarget := null // target of assignment + Expr? assignIndex := null // indexed assign: index + + private Expr findAssignTarget(Expr expr) + { + if (expr is LocalVarExpr || expr is FieldExpr) return expr + t := Type.of(expr).field("target", false) + if (t != null) return findAssignTarget(t.get(expr)) + throw err("No base Expr found", loc) + } + + private ShortcutExpr findIndexedAssign(Expr expr) + { + if (expr is ShortcutExpr) return expr + t := Type.of(expr).field("target", false) + if (t != null) return findIndexedAssign(t.get(expr)) + throw err("No base Expr found", loc) + } + + override Void write() + { + if (fieldSet) + { + return super.write + } + if (isIndexedAssign) + { + return doWriteIndexedAssign + } + if (se.isPostfixLeave) + { + var := uniqName + old := plugin.thisName + plugin.thisName = "this\$" + js.w("((this\$) => { let ${var} = ", loc) + writeExpr(assignTarget); js.w(";") + doWrite + js.w("; return ${var}; })(${old})", loc) + plugin.thisName = old + } + else doWrite + } + + private Void doWrite() + { + if (se.isAssign) + { + if (assignTarget is FieldExpr) + { + fieldSet = true + fe := (FieldExpr)assignTarget + JsExpr(plugin, fe).writeSetter(this) + fieldSet = false + return + } + else + { + writeExpr(assignTarget) + js.w(" = ", loc) + } + } + super.write + } + + private Void doWriteIndexedAssign() + { + newVal := uniqName + oldVal := uniqName + ref := uniqName + index := uniqName + old := plugin.thisName + retVal := isPostfixLeave ? oldVal : newVal + plugin.thisName = "this\$" + js.w("((this\$) => { ", loc) + js.w("let ${ref} = ", loc); writeExpr(assignTarget); js.w("; ") + js.w("let ${index} = ", loc); writeExpr(assignIndex); js.w("; ") + js.w("let ${newVal} = ", loc); super.write; js.w("; ") + if (isPostfixLeave) js.w("let ${oldVal} = ${ref}.get(${index}); ", loc) + js.w("${ref}.set(${index},${newVal}); ", loc) + js.w(" return ${retVal}; })(${old})", loc) + plugin.thisName = old + } +} \ No newline at end of file diff --git a/src/compilerEs/fan/ast/JsNode.fan b/src/compilerEs/fan/ast/JsNode.fan new file mode 100644 index 000000000..8341ef02a --- /dev/null +++ b/src/compilerEs/fan/ast/JsNode.fan @@ -0,0 +1,210 @@ +// +// 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 + +** +** JsNode +** +abstract class JsNode +{ + new make(CompileEsPlugin plugin, Node? node := null) + { + this.plugin = plugin + this.nodeRef = node + } + + CompileEsPlugin plugin { private set } + Compiler c() { plugin.compiler } + private Node? nodeRef + + virtual Node? node() { nodeRef } + virtual Loc? loc() { node?.loc } + static Loc? toLoc(Obj obj) { obj is Node ? ((Node)obj).loc : null } + + abstract Void write() + + JsWriter js() { plugin.js } + +////////////////////////////////////////////////////////////////////////// +// Type Utils +////////////////////////////////////////////////////////////////////////// + + Bool isJsType(TypeDef def) + { + // we inline closures directly, so no need to generate anonymous types + if (def.isClosure) return false + + // TODO:FIXIT: do we still need this? + if (def.qname.contains("\$Cvars")) + { + echo("WARN: Cvar class: ${def.qname}") + return false + } + + // check for @Js facet or if forced generation + return def.hasFacet("sys::Js") || c.input.forceJs + } + + Bool checkJsSafety(CType ctype, Loc? loc) + { + if (ctype is TypeRef) return checkJsSafety(ctype->t, loc) + else if (ctype is NullableType) return checkJsSafety(ctype->root, loc) + else if (ctype is ListType) return checkJsSafety(ctype->v, loc) + else if (ctype is MapType) + { + return checkJsSafety(ctype->k, loc) && checkJsSafety(ctype->v, loc) + } + else if (ctype is FuncType) + { + safe := true + ft := (FuncType)ctype + ft.params.each |param| { safe = safe && checkJsSafety(param, loc) } + safe = safe && checkJsSafety(ft.ret, loc) + return safe + } + else if (!(ctype.pod.name == "sys" || ctype.isSynthetic || ctype.facet("sys::Js") != null || c.input.forceJs)) + { + warn("Type '${ctype.qname}' not available in JS", loc) + return false + } + return true + } + +////////////////////////////////////////////////////////////////////////// +// Method Utils +////////////////////////////////////////////////////////////////////////// + + ** generates '(p1, p2, ...pn)' + Str methodParams(CParam[] params) + { + buf := StrBuf().addChar('(') + params.each |param, i| + { + if (i > 0) buf.addChar(',') + buf.add(nameToJs(param.name)) + } + return buf.addChar(')').toStr + } + +////////////////////////////////////////////////////////////////////////// +// Name Utils +////////////////////////////////////////////////////////////////////////// + + ** Get the module-qualified name for this CType. If the type is in the + ** this pod, it does not need to be qualified + Str qnameToJs(CType ctype) + { + podName := ctype.pod.name + thisPod := podName == plugin.pod.name + js := thisPod ? ctype.name : "${plugin.podAlias(podName)}.${ctype.name}" + + // make it so java FFI calls parse in js runtimes + // code will parse but fail if actually invoked + if (js.contains(".[java].")) js = js.replace(".[java].", ".") + else if (js.contains("[java]")) js = js.replace("[java]", "java.fail") + + return js + } + + ** Get the name that should be used for the generated field in JS code + static Str fieldJs(Obj name) + { + // if (name is Str) return "_${name}\$" + if (name is Str) return "#${name}" + if (name is Field) return fieldJs(((Field)name).name) + if (name is FieldDef) return fieldJs(((FieldDef)name).name) + throw ArgErr("${name} [${name.typeof}]") + } + + ** Return the JS variable/method/param name to use for the given Fantom name + ** + ** Note - use fieldJs for generating field names since we have a lot of special + ** handling for fields + Str nameToJs(Str name) { pickleName(name, plugin.dependOnNames) } + + @NoDoc static Str pickleName(Str name, Obj? depends := null) + { + name = namePickles.get(name, name) + if (depends != null) + { + isDepends := false + if (depends is Map) isDepends = ((Str:Bool)depends).get(name) + else if (depends is List) isDepends = ((List)depends).contains(name) + if (isDepends) name = "\$${name}" + } + return name + } + + private static const Str:Str namePickles + static + { + m := Str:Str[:] + ["char", "const", "delete", "enum", "export", "float", "import", "in", "int", + "interface", "let", "self", "require", "typeof", "var", "with", + ].each |name| { m[name] = "${name}\$" } + namePickles = m.toImmutable + } + + ** return a unique id name + Str uniqName(Str name := "u") + { + "\$_${name}${plugin.nextUid}" + } + +////////////////////////////////////////////////////////////////////////// +// Logging +////////////////////////////////////////////////////////////////////////// + + CompilerErr err(Str msg, Loc? loc := null) { plugin.err(msg, loc) } + CompilerErr warn(Str msg, Loc? loc := null) { plugin.warn(msg, loc) } + +////////////////////////////////////////////////////////////////////////// +// General Utils +////////////////////////////////////////////////////////////////////////// + + Bool isPrimitive(CType ctype) { pmap.get(ctype.qname, false) } + const Str:Bool pmap := + [ + "sys::Bool": true, + "sys::Decimal": true, + "sys::Float": true, + "sys::Int": true, + "sys::Num": true, + "sys::Str": true + ] + + Void writeBlock(Block? block) + { + if (block == null) return + block.stmts.each |stmt| { + writeStmt(stmt) + js.wl(";") + } + } + + Void writeStmt(Stmt? stmt) + { + if (stmt == null) return + JsStmt(plugin, stmt).write + } + + Void writeExpr(Expr? expr) + { + if (expr == null) return + switch (expr.id) + { + // case ExprId.call: JsCallExpr(plugin, expr).write + // case ExprId.shortcut: JsShortcutExpr(plugin, expr).write + default: JsExpr(plugin, expr).write + } + } + + TypeDef? curType() { plugin.curType } + +} \ No newline at end of file diff --git a/src/compilerEs/fan/ast/JsPod.fan b/src/compilerEs/fan/ast/JsPod.fan new file mode 100644 index 000000000..d85fc01b7 --- /dev/null +++ b/src/compilerEs/fan/ast/JsPod.fan @@ -0,0 +1,256 @@ +// +// 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 + +** +** JsPod +** +class JsPod : JsNode +{ + +////////////////////////////////////////////////////////////////////////// +// Constructor +////////////////////////////////////////////////////////////////////////// + + new make(CompileEsPlugin plugin) : super(plugin) + { + this.pod = plugin.pod + + // map native files by name + c.jsFiles?.each |f| { + // we expect ES javascript files in es/ directory + natives[f.name] = f.parent.parent.plus(`es/${f.name}`) + } + + // find types to emit + c.types.findAll { isJsType(it) }.each { types.add(JsType(plugin, it)) } + } + + private PodDef pod + private JsType[] types := [,] + private [Str:File] natives := [:] + private [Str:Bool] peers := [:]{ it.def = false } + +////////////////////////////////////////////////////////////////////////// +// JsPod +////////////////////////////////////////////////////////////////////////// + + override Void write() + { + writeRequire + writeTypes + writeTypeInfo + writeProps + writeNatives + writeExports + js.wl("}).call(this);") + } + + private Void writeRequire() + { + js.wl("// cjs require begin") + CommonJs.moduleStart.splitLines.each |line| { js.wl(line) } + + js.wl("const fan = __require('fan.js');") + js.wl("const fantom = __require('fantom.js');") + js.wl("const sys = fantom ? fantom.sys : __require('sys.js');") + + // special handling for dom + // if (pod.name == "dom") js.wl("const es6 = __require('es6.js');") + + pod.depends.each |depend| + { + if (depend.name == "sys") return + // NOTE if we change sys to fan we need to update JNode.qnameToJs + // js.wl("import * as ${depend.name} from './${depend.name}.js';") + js.wl("const ${plugin.podAlias(depend.name)} = __require('${depend.name}.js');") + } + + js.wl("// cjs require end") + js.wl("const js = (typeof window !== 'undefined') ? window : global;") + } + + // private Void writeImports() + // { + // // special handling for dom + // if (pod.name == "dom") + // { + // js.wl("import * as es6 from './es6.js'") + // } + + // pod.depends.each |depend| + // { + // // NOTE if we change sys to fan we need to update JNode.qnameToJs + // // js.wl("import * as ${depend.name} from './${depend.name}.js';") + // if (Pod.find(depend.name).file(`/esm/${depend.name}.js`, false) != null) + // js.wl("import * as ${depend.name} from './${depend.name}.js';") + // else + // { + // // TODO: FIXIT - non-js dependencies that will only be there in node env + // // but not the browser. Maybe the browser should return empty export in + // // this case? or we could put a comment on the same line that we + // // could search for and strip out before serving the js in the browser. + // // js.wl("let ${depend.name};") + // // await import('./esm/testSys.js').then(obj => testSys = obj).catch(err => {}); + // // js.wl("await import('./${depend.name}.js').then(obj => ${depend.name}=obj).catch(err => {});") + + // js.wl("import * as ${depend.name} from './${depend.name}.js';") + // } + + + // // if (depend.name == "sys") + // // js.wl("import * as fan from './sys.js';") + // // else + // // js.wl("import * as ${depend.name} from './${depend.name}.js") + // } + // js.nl + // } + + private Void writeTypes() + { + types.each |JsType t| + { + plugin.curType = t.def + if (t.def.isNative) writePeer(t, null) + else + { + t.write + if (t.hasNatives) writePeer(t, t.peer) + } + js.nl + plugin.curType = null + } + } + + private Void writePeer(JsType t, CType? peer) + { + key := "${t.name}.js" + if (peer != null) + { + key = "${peer.name}Peer.js" + this.peers[t.name] = true + } + + file := natives[key] + if (file == null) + { + err("Missing native impl for ${t.def.signature}", Loc("${t.name}.fan")) + // Do not export peer types that we don't have implementations for + this.peers[t.name] = false + } + else + { + in := file.in + js.minify(in) + natives.remove(key) + } + } + + private Void writeTypeInfo() + { + // add the pod to the type system + js.wl("const p = sys.Pod.add\$('${pod.name}');") + js.wl("const xp = sys.Param.noParams\$();") + // general use map variable + js.wl("let m;") + + // filter out synthetic types from reflection + reflect := types.findAll |t| { !t.def.isSynthetic } + + // write all types first + reflect.each |t| + { + adder := t.def.isMixin ? "p.am\$" : "p.at\$" + base := "${t.base.pod}::${t.base.name}" + mixins := t.mixins.join(",") |m| { "'${m.pod}::${m.name}'" } + facets := toFacets(t.facets) + flags := t.def.flags + js.wl("${t.name}.type\$ = ${adder}('${t.name}','${base}',[${mixins}],{${facets}},${flags},${t.name});") + } + + // then write slot info + reflect.each |JsType t| + { + if (t.fields.isEmpty && t.methods.isEmpty) return + js.w("${t.name}.type\$") + t.fields.each |FieldDef f| + { + // don't write for FFI + if (f.isForeign || f.fieldType.isForeign) return + + facets := toFacets(f.facets) + js.w(".af\$('${f.name}',${f->flags},'${f.fieldType.signature}',{${facets}})") + } + t.methods.each |MethodDef m| + { + if (m.isFieldAccessor) return + if (m.params.any |CParam p->Bool| { p.paramType.isForeign }) return + params := m.params.join(",") |p| { "new sys.Param('${p.name}','${p.paramType.signature}',${p.hasDefault})"} + paramList := m.params.isEmpty + ? "xp" + : "sys.List.make(sys.Param.type\$,[${params}])" + facets := toFacets(m.facets) + js.w(".am\$('${m.name}',${m.flags},'${m.ret.signature}',${paramList},{${facets}})") + } + js.wl(";") + } + + // pod meta + js.nl.wl("m=sys.Map.make(sys.Str.type\$,sys.Str.type\$);") + pod.meta.each |v, k| + { + js.wl("m.set(${k.toCode}, ${v.toCode});") + } + js.wl("p.__meta(m);").nl + } + + private static Str toFacets(FacetDef[]? facets) + { + facets == null ? "" : facets.join(",") |f| { "'${f.type.qname}':${f.serialize.toCode}" } + } + + private Void writeProps() + { + baseDir := c.input.baseDir + if (baseDir != null) + { + c.resFiles.each |file| + { + if (file.ext != "props") return + uri := file.uri.relTo(baseDir.uri) + key := "${pod.name}:${uri}" + js.wl("m=sys.Map.make(sys.Str.type\$, sys.Str.type\$);") + file.in.readProps.each |v,k| { js.wl("m.set(${k.toCode},${v.toCode});") } + js.wl("sys.Env.cur().__props(${key.toCode}, m);").nl + } + js.nl + } + } + + private Void writeNatives() + { + natives.each |f| { js.minify(f.in) } + } + + private Void writeExports() + { + // only export public types + js.wl("// cjs exports begin") + js.wl("const ${pod.name} = {").indent + types.findAll { it.def.isPublic }.each |t| { + js.wl("${t.name},") + if (this.peers[t.name]) js.wl("${t.peer.name}Peer,") + } + js.unindent.wl("};") + js.wl("fan.${pod.name} = ${pod.name};") + js.wl("if (typeof exports !== 'undefined') module.exports = ${pod.name};") + // js.wl("else this.${pod.name} = ${pod.name};") + js.wl("// cjs exports end") + } +} \ No newline at end of file diff --git a/src/compilerEs/fan/ast/JsStmt.fan b/src/compilerEs/fan/ast/JsStmt.fan new file mode 100644 index 000000000..0acca08f7 --- /dev/null +++ b/src/compilerEs/fan/ast/JsStmt.fan @@ -0,0 +1,238 @@ +// +// Copyright (c) 2023, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 15 May 2023 Matthew Giannini Creation +// + +using compiler + +** +** JsStmt +** +class JsStmt : JsNode +{ + new make(CompileEsPlugin plugin, Stmt stmt) : super(plugin, stmt) + { + } + + override Stmt? node() { super.node } + Stmt stmt() { this.node } + + override Void write() + { + switch (stmt.id) + { + case StmtId.nop: return + case StmtId.expr: writeExprStmt(stmt) + case StmtId.localDef: writeLocalDefStmt(stmt) + case StmtId.ifStmt: writeIfStmt(stmt) + case StmtId.returnStmt: writeReturnStmt(stmt) + case StmtId.throwStmt: writeThrowStmt(stmt) + case StmtId.forStmt: writeForStmt(stmt) + case StmtId.whileStmt: writeWhileStmt(stmt) + case StmtId.breakStmt: js.w("break") + case StmtId.continueStmt: js.w("continue") + case StmtId.tryStmt: writeTryStmt(stmt) + case StmtId.switchStmt: writeSwitchStmt(stmt) + default: + stmt.print(AstWriter()) + throw err("Unknown StmtId: ${stmt.id} ${stmt.typeof}", stmt.loc) + } + } + +////////////////////////////////////////////////////////////////////////// +// Expr +////////////////////////////////////////////////////////////////////////// + + private Void writeExprStmt(ExprStmt stmt) + { + writeExpr(stmt.expr) + } + +////////////////////////////////////////////////////////////////////////// +// LocalDef +////////////////////////////////////////////////////////////////////////// + + private Void writeLocalDefStmt(LocalDefStmt stmt) + { + // don't write def for catch vars since we handle that ourselves in writeCatches() + if (stmt.isCatchVar) return + + js.w("let ", loc) + if (stmt.init == null) js.w(stmt.name, loc) + else + { + JsExpr(plugin, stmt.init) { it.isLocalDefStmt = true }.write + } + } + +////////////////////////////////////////////////////////////////////////// +// If +////////////////////////////////////////////////////////////////////////// + + private Void writeIfStmt(IfStmt stmt) + { + js.w("if ("); writeExpr(stmt.condition); js.wl(") {") + js.indent + writeBlock(stmt.trueBlock) + js.unindent.wl("}") + if (stmt.falseBlock != null) + { + js.wl("else {") + js.indent + writeBlock(stmt.falseBlock) + js.unindent.wl("}") + } + } + +////////////////////////////////////////////////////////////////////////// +// Return +////////////////////////////////////////////////////////////////////////// + + private Void writeReturnStmt(ReturnStmt stmt) + { + js.w("return", loc) + if (stmt.expr != null) + { + js.w(" ") + writeExpr(stmt.expr) + } + } + +////////////////////////////////////////////////////////////////////////// +// Throw +////////////////////////////////////////////////////////////////////////// + + private Void writeThrowStmt(ThrowStmt ts) + { + js.w("throw ") + writeExpr(ts.exception) + } + +////////////////////////////////////////////////////////////////////////// +// For +////////////////////////////////////////////////////////////////////////// + + private Void writeForStmt(ForStmt fs) + { + js.w("for ("); writeStmt(fs.init); js.w("; ") + writeExpr(fs.condition); js.w("; ") + writeExpr(fs.update); js.wl(") {").indent + writeBlock(fs.block) + js.unindent.wl("}") + } + +////////////////////////////////////////////////////////////////////////// +// While +////////////////////////////////////////////////////////////////////////// + + private Void writeWhileStmt(WhileStmt ws) + { + js.w("while ("); writeExpr(ws.condition); js.wl(") {").indent + writeBlock(ws.block) + js.unindent.wl("}") + } + +////////////////////////////////////////////////////////////////////////// +// Try +////////////////////////////////////////////////////////////////////////// + + private Void writeTryStmt(TryStmt ts) + { + js.wl("try {").indent + writeBlock(ts.block) + js.unindent.wl("}") + + writeCatches(ts.catches) + + if (ts.finallyBlock != null) + { + js.wl("finally {").indent + writeBlock(ts.finallyBlock) + js.unindent.wl("}") + } + } + + private Void writeCatches(Catch[] catches) + { + if (catches.isEmpty) return + + var := uniqName + hasTyped := catches.any |c| { c.errType != null } + hasCatchAll := catches.any |c| { c.errType == null } + + js.wl("catch (${var}) {").indent + if (hasTyped) js.wl("${var} = sys.Err.make(${var});") + + doElse := false + catches.each |c| + { + if (c.errType != null) + { + qname := qnameToJs(c.errType) + cVar := c.errVariable ?: uniqName + if (doElse) js.w("else ") + else doElse = true + + js.wl("if (${var} instanceof ${qname}) {").indent + js.wl("let ${cVar} = ${var};") + writeBlock(c.block) + js.unindent.wl("}") + } + else + { + hasElse := catches.size > 1 + if (hasElse) js.wl("else {").indent + writeBlock(c.block) + if (hasElse) js.unindent.wl("}") + } + } + + if (!hasCatchAll) + { + js.wl("else {").indent + js.wl("throw ${var};") + js.unindent.wl("}") + } + + js.unindent.wl("}") + } + +////////////////////////////////////////////////////////////////////////// +// Switch +////////////////////////////////////////////////////////////////////////// + + private Void writeSwitchStmt(SwitchStmt ss) + { + var := uniqName + js.w("let ${var} = "); writeExpr(ss.condition); js.wl(";") + + ss.cases.each |c, i| + { + if (i > 0) js.w("else ") + js.w("if (") + c.cases.each |e, j| + { + if (j > 0) js.w(" || ") + js.w("sys.ObjUtil.equals(${var}, "); writeExpr(e); js.w(")") + } + js.wl(") {").indent + writeBlock(c.block) + js.unindent.wl("}") + } + + if (ss.defaultBlock != null) + { + if (!ss.cases.isEmpty) + { + js.wl("else {").indent + writeBlock(ss.defaultBlock) + js.unindent.wl("}") + + } + else writeBlock(ss.defaultBlock) + } + } +} \ No newline at end of file diff --git a/src/compilerEs/fan/ast/JsType.fan b/src/compilerEs/fan/ast/JsType.fan new file mode 100644 index 000000000..0035a007c --- /dev/null +++ b/src/compilerEs/fan/ast/JsType.fan @@ -0,0 +1,473 @@ +// +// 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) 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 := nameToJs(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 := fieldJs(f.name) + accessName := nameToJs(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 := nameToJs(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)].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) + js.nl + plugin.curMethod = null + } + + private Void doWriteMethod(MethodDef m, Str methName := nameToJs(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 + hasClosure := ClosureFinder(m).exists + if (hasClosure) js.wl("const this\$ = ${plugin.thisName};") + + if (m.isNative) + { + if (m.isStatic) + { + js.wl("return ${qnameToJs(peer)}Peer.${methName}${methodParams(methParams)};", m.loc) + } + else + { + pars := CParam[SyntheticParam("this")].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("}") + } + + ** 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 wrapped in + // if (true) {...} + // blocks 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 .#vals field directly + js.wl("const ${uniqName} = ${enumName}.vals();") + m.code.stmts.each |stmt| + { + if (stmt is IfStmt) { writeStmt(stmt) } + } + 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) { this.name = name } + override const Str name + override CType paramType() { throw UnsupportedErr() } + 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) + } +} diff --git a/src/compilerEs/fan/util/Base64VLQ.fan b/src/compilerEs/fan/util/Base64VLQ.fan new file mode 100644 index 000000000..36a3d3f11 --- /dev/null +++ b/src/compilerEs/fan/util/Base64VLQ.fan @@ -0,0 +1,163 @@ +// +// Copyright (c) 2015, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 06 Jul 15 Matthew Giannini Creation +// + +class Base64VLQ +{ + private const static Int VLQ_BASE_SHIFT := 5 + private const static Int VLQ_BASE := 1.shiftl(VLQ_BASE_SHIFT) + private const static Int VLQ_BASE_MASK := VLQ_BASE - 1 // (11111) + private const static Int VLQ_CONTINUATION_BIT := VLQ_BASE + + private const static Str BASE64_MAP := + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/" + + static Str encode(Int val) + { + buf := StrBuf() + val = toVLQSigned(val) + while (true) + { + digit := val.and(VLQ_BASE_MASK) + val = val.shiftr(VLQ_BASE_SHIFT) + if (val > 0) + digit = digit.or(VLQ_CONTINUATION_BIT) + if (digit < 0 || digit > 63) throw Err("${digit} out of range for ${val}") + buf.addChar(BASE64_MAP[digit]) + if (val == 0) break + } + return buf.toStr + } + + ** Converts a two-complement value to a value where the sign bit is + ** placed in the least significant bit. + private static Int toVLQSigned(Int val) + { + if (val < 0) + return (-val).shiftl(1) + 1 + else + return val.shiftl(1) + 0 + } +} +/* +/* + * Copyright 2011 The Closure Compiler Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.debugging.sourcemap; + +import java.io.IOException; + +/** + * We encode our variable length numbers as base64 encoded strings with + * the least significant digit coming first. Each base64 digit encodes + * a 5-bit value (0-31) and a continuation bit. Signed values can be + * represented by using the least significant bit of the value as the + * sign bit. + * + * @author johnlenz@google.com (John Lenz) + */ +final class Base64VLQ { + // Utility class. + private Base64VLQ() {} + + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + private static final int VLQ_BASE_SHIFT = 5; + private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + private static final int VLQ_BASE_MASK = VLQ_BASE-1; + + // The continuation bit is the 6th bit. + private static final int VLQ_CONTINUATION_BIT = VLQ_BASE; + + /** + * Converts from a two-complement value to a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + */ + private static int toVLQSigned(int value) { + if (value < 0) { + return ((-value) << 1) + 1; + } else { + return (value << 1) + 0; + } + } + + /** + * Converts to a two-complement value from a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 + * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 + */ + private static int fromVLQSigned(int value) { + boolean negate = (value & 1) == 1; + value = value >> 1; + return negate ? -value : value; + } + + /** + * Writes a VLQ encoded value to the provide appendable. + * @throws IOException + */ + public static void encode(Appendable out, int value) + throws IOException { + value = toVLQSigned(value); + do { + int digit = value & VLQ_BASE_MASK; + value >>>= VLQ_BASE_SHIFT; + if (value > 0) { + digit |= VLQ_CONTINUATION_BIT; + } + out.append(Base64.toBase64(digit)); + } while (value > 0); + } + + /** + * A simple interface for advancing through a sequence of characters, that + * communicates that advance back to the source. + */ + interface CharIterator { + boolean hasNext(); + char next(); + } + + /** + * Decodes the next VLQValue from the provided CharIterator. + */ + public static int decode(CharIterator in) { + int result = 0; + boolean continuation; + int shift = 0; + do { + char c = in.next(); + int digit = Base64.fromBase64(c); + continuation = (digit & VLQ_CONTINUATION_BIT) != 0; + digit &= VLQ_BASE_MASK; + result = result + (digit << shift); + shift = shift + VLQ_BASE_SHIFT; + } while (continuation); + + return fromVLQSigned(result); + } +} +*/ diff --git a/src/compilerEs/fan/util/JsExtToMime.fan b/src/compilerEs/fan/util/JsExtToMime.fan new file mode 100644 index 000000000..d3fc9648b --- /dev/null +++ b/src/compilerEs/fan/util/JsExtToMime.fan @@ -0,0 +1,31 @@ +// +// Copyright (c) 2020, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 11 Jul 2023 Matthew Giannini Creation +// + +** +** JsExtToMime +** +class JsExtToMime +{ + new make(ModuleSystem ms) { this.ms = ms } + + private ModuleSystem ms + + Void write(OutStream out) + { + ms.writeBeginModule(out) + ms.writeInclude(out, "sys.ext") + + props := Env.cur.findFile(`etc/sys/ext2mime.props`).readProps + out.printLine("const c=sys.MimeType.__cache;") + props.each |mime, ext| + { + out.printLine("c(${ext.toCode},${mime.toCode});") + } + ms.writeEndModule(out) + } +} diff --git a/src/compilerEs/fan/util/JsUnitDatabase.fan b/src/compilerEs/fan/util/JsUnitDatabase.fan new file mode 100644 index 000000000..5dbc88a95 --- /dev/null +++ b/src/compilerEs/fan/util/JsUnitDatabase.fan @@ -0,0 +1,68 @@ +// +// Copyright (c) 2010, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 27 Jul 2010 Andy Frank Creation +// 07 Jul 2023 Matthew Giannini Refactor for ES +// + +** +** JsUnitDatabase +** +class JsUnitDatabase +{ + new make(ModuleSystem ms) { this.ms = ms } + + private ModuleSystem ms; + + Void write(OutStream out) + { + ms.writeBeginModule(out) + ms.writeInclude(out, "sys.ext") + + // open etc/sys/units.txt + in := Env.cur.findFile(`etc/sys/units.txt`).in + out.printLine("const qn=sys.List.make(sys.Str.type\$,[]);") + out.printLine("let q;") + + // parse each line + curQuantityName := "" + in.readAllLines.each |line| + { + // skip comment and blank lines + line = line.trim + if (line.startsWith("//") || line.size == 0) return + + // quanity sections delimited as "-- name (dim)" + if (line.startsWith("--")) + { + name := line[2.. 0) + { + out.printLine("sys.Unit.__quantityUnits('${curQuantityName}', q);\n") + } + + // start new def + curQuantityName = name + out.printLine( + "// $curQuantityName + qn.add('${curQuantityName}'); + q = sys.List.make(sys.Unit.type\$, []);") + } + return + } + + // add unit + out.printLine("q.add(sys.Unit.define('${line}'));") + } + + // finish up + out.printLine("sys.Unit.__quantities(qn);") + ms.writeEndModule(out) + } +} + diff --git a/src/compilerEs/fan/util/SourceMap.fan b/src/compilerEs/fan/util/SourceMap.fan new file mode 100644 index 000000000..deaa6da2f --- /dev/null +++ b/src/compilerEs/fan/util/SourceMap.fan @@ -0,0 +1,316 @@ +// +// Copyright (c) 2015, Brian Frank and Andy Frank +// Licensed under the Academic Free License version 3.0 +// +// History: +// 02 Jul 15 Matthew Giannini Creation +// + +using compiler + +class SourceMap +{ + +////////////////////////////////////////////////////////////////////////// +// Constructor +////////////////////////////////////////////////////////////////////////// + + new make(CompilerSupport support) + { + this.support = support + this.c = support.compiler + } + +////////////////////////////////////////////////////////////////////////// +// SourceMap +////////////////////////////////////////////////////////////////////////// + + This add(Str text, Loc genLoc, Loc srcLoc, Str? name := null) + { + // map source + File? source := files.getOrAdd(srcLoc.file) |->File?| { findSource(srcLoc) } + if (source == null) return this + + // add map field + fields.add(MapField(text, genLoc, srcLoc, name)) + return this + } + + private File? findSource(Loc loc) + { + c.srcFiles?.find { it.osPath == File.os(loc.file).osPath } + } + +////////////////////////////////////////////////////////////////////////// +// Output +////////////////////////////////////////////////////////////////////////// + + Void write(Int lineCount, OutStream out := Env.cur.out) + { + pod := support.pod.name + out.writeChars("{\n") + out.writeChars("\"version\": 3,\n") + out.writeChars("\"file\": \"${pod}.js\",\n") + out.writeChars("\"x_fan_linecount\": $lineCount,\n") + out.writeChars("\"sourceRoot\": \"/dev/${pod}/\",\n") + writeSources(out) + writeMappings(out) + out.writeChars("}\n") + out.flush + } + + private Void writeSources(OutStream out) + { + // write sources + out.writeChars("\"sources\": [") + files.vals.each |file, i| + { + if (i > 0) out.writeChars(",") + if (file == null) out.writeChars("null") + else out.writeChars("\"${file.name}\"") + } + out.writeChars("],\n") + } + + private Void writeMappings(OutStream out) + { + // map source index + srcIdx := [Str:Int][:] + files.keys.each |k, i| { srcIdx[k] = i } + + out.writeChars("\"mappings\": \"") + prevFileIdx := 0 + prevSrcLine := 0 + prevSrcCol := 0 + prevGenLine := 0 + prevGenCol := 0 + MapField? prevField + fields.each |MapField f, Int i| + { + fileIdx := srcIdx[f.srcLoc.file] + genLine := f.genLoc.line + genCol := f.genLoc.col + srcLine := f.srcLine + srcCol := f.srcCol + if (genLine < prevGenLine) throw Err("${f} is before line ${prevGenLine}") + + // handle missing/blank lines + if (genLine != prevGenLine) + { + prevGenCol = 0 + while (genLine != prevGenLine) + { + out.writeChar(';') + ++prevGenLine + } + } + else + { + if (i > 0) + { + if (genCol <= prevGenCol) throw Err("${genCol} is before col ${prevGenCol}") + out.writeChar(',') + } + } + + // calculate diffs + genColDiff := genCol - prevGenCol + fileDiff := fileIdx - prevFileIdx + srcLineDiff := srcLine - prevSrcLine + srcColDiff := srcCol - prevSrcCol + + // write segment field + out.writeChars(Base64VLQ.encode(genColDiff)) + .writeChars(Base64VLQ.encode(fileDiff)) + .writeChars(Base64VLQ.encode(srcLineDiff)) + .writeChars(Base64VLQ.encode(srcColDiff)) + + // update prev state + prevGenCol = genCol + prevFileIdx = fileIdx + prevSrcLine = srcLine + prevSrcCol = srcCol + } + out.writeChars(";\"\n") + } + +////////////////////////////////////////////////////////////////////////// +// Pack +////////////////////////////////////////////////////////////////////////// + + ** Compile a list of pod JavaScript files into a single unified source + ** map file. The list of files passed to this method should match + ** exactly the list of files used to create the corresponding JavaScript + ** FilePack. If the file is the standard pod JS file, then we will include + ** an offset version of "{pod}.js.map" generated by the JavaScript compiler. + ** Otherwise if the file is another JavaScript file (such as units.js) then + ** we just add the appropiate offset. + ** + ** The 'sourceRoot' option may be passed in to replace "/dev/{podName}" + ** as the root URI used to fetch source files from the server. + static Void pack(File[] files, OutStream out, [Str:Obj]? options := null) + { + // options + sourceRoot := options?.get("sourceRoot") as Str + + // open compound source map file + out.printLine("{") + .printLine("\"version\": 3,") + .printLine("\"sections\": [") + + // process each file + curOffset := 0 + files.each |file, i| + { + // check if file is within a pod + uri := file.uri + pod := uri.scheme == "fan" ? Pod.find(uri.host, false) : null + + // check if this standard pod JS file + Str? json := null + if (pod != null && isPodJsFile(file)) + { + // lookup sourcemap for the pod + sm := pod.file(`/${pod.name}.js.map`, false) + if (sm != null) + { + // read into memory + json = sm.readAllStr + + // apply options + if (sourceRoot != null) json = setSourceRoot(json, sourceRoot+pod.name) + + } + } + + // read number of lines from JSON if we can, otherwise count them + // echo("-- $uri.name " + readNumLinesFromJson(json) + " ?= " + readNumLinesByCounting(file)) + numLines := readNumLinesFromJson(json) ?: readNumLinesByCounting(file) + + // if we have raw js file, then generate a synthetic sourcemap + if (json == null) + { + mappings := StrBuf().add("AAAA;") + buf := StrBuf() + buf.add("""{ + "version":3, + "file": "core.js", + "sources": ["${file.name}"], + """) + if (pod != null) buf.add("\"sourceRoot\": \"").add(sourceRoot ?: "/dev/").add(pod.name).add("/\",\n") + buf.add(Str<|"mappings": "AAAA;|>) + (numLines+1).times |x| { buf.add("AACA;") } + buf.add("\"}") + json = buf.toStr + } + + // add offset section and insert original JSON + out.print(Str<|{"offset": {"line":|>) + .print(curOffset) + .print(Str<|, "column":0}, "map":|>) + .print(json) + .print("}") + if (i+1 < files.size) out.printLine(",") // cannot have trailing comma + + + // advance curOffset + curOffset += numLines + } + + // close file + out.printLine("]") + .printLine("}") + } + + ** Return if the file is the standard compilerJs pod transpiled source + private static Bool isPodJsFile(File f) + { + f.uri.scheme == "fan" && f.uri.pathStr == "/${f.uri.host}.js" + } + + ** Try to parse "x_fan_linecount" key from JSON contents + private static Int? readNumLinesFromJson(Str? json) + { + r := findKeyValRange(json, Str<|"x_fan_linecount":|>) + if (r == null) return null + return json.getRange(r).toInt(10, false) + } + + ** Fallback is to read each line to determine line count + private static Int readNumLinesByCounting(File file) + { + num := 0 + file.eachLine { ++num } + return num + } + + ** Set source root option + private static Str setSourceRoot(Str json, Str sourceRoot) + { + r := findKeyValRange(json, Str<|"sourceRoot":|>) + if (r == null) return json + return json[0.. b.name } } + } + + ** Get the continent name from the full name, or "" + ** if the full name doesn't have a continent. + private Str continent(Str fullName) + { + fullName.contains("/") ? fullName.split('/').first : "" + } + + private Void writeTzJs() + { + jsOut := js.out + try + { + typeRef := this.embed ? "TimeZone" : "sys.TimeZone" + if (!embed) + { + jsOut.printLine("import * as sys from './sys.js'"); + } + jsOut.printLine("const c=${typeRef}.__cache;") + + // write built-in timezones + byContinent.each |TimeZone[] timezones, Str continent| + { + timezones.each |TimeZone tz| + { + log.debug("$tz.fullName") + encoded := encodeTimeZone(tz) + jsOut.printLine("c(${tz.fullName.toCode},${encoded.toBase64.toCode});") + } + } + + // write aliases + jsOut.printLine("const a=${typeRef}.__alias;") + aliases.each |target, alias| + { + log.debug("Alias $alias = $target") + jsOut.printLine("a(${alias.toCode},${target.toCode});") + } + } + finally jsOut.close + log.info("Wrote: ${js.osPath ?: js}") + } + + private Buf encodeTimeZone(TimeZone tz) + { + buf := Buf().writeUtf(tz.fullName); + rules := ([Str:Obj][])tz->rules + rules.each |r| { encodeRule(r, buf.out) } + return buf + } + + private Void encodeRule(Str:Obj r, OutStream out) + { + dstOffset := r["dstOffset"] + out.writeI2(r["startYear"]) + .writeI4(r["offset"]) + .writeUtf(r["stdAbbr"]) + .writeI4(dstOffset) + if (dstOffset != 0) + { + out.writeUtf(r["dstAbbr"]) + encodeDst(r["dstStart"], out) + encodeDst(r["dstEnd"], out) + } + } + + private Void encodeDst(Str:Obj dst, OutStream out) + { + out.write(dst["mon"]) + .write(dst["onMode"]) + .write(dst["onWeekday"]) + .write(dst["onDay"]) + .writeI4(dst["atTime"]) + .write(dst["atMode"]) + } + + +////////////////////////////////////////////////////////////////////////// +// Args +////////////////////////////////////////////////////////////////////////// + + private Bool gen := false + private Bool embed := false + + private Void parseArgs() + { + if (args.isEmpty) usage() + i :=0 + while (i < args.size) + { + arg := args[i++] + switch (arg) + { + case "-gen": + this.gen = true + case "-embed": + this.embed = true + case "-outDir": + outDir := args[i++].toUri.toFile + if (!outDir.isDir) throw ArgErr("Not a directory: ${outDir}") + this.js = outDir.plus(`fan_tz.js`) + case "-silent": + this.log.level = LogLevel.silent + case "-verbose": + case "-v": + log.level = LogLevel.debug + case "-help": + case "-?": + usage() + default: + Env.cur.err.printLine("Bad option: ${arg}") + usage() + } + } + } + + private Void usage() + { + out := Env.cur.out + main := Env.cur.mainMethod?.parent?.name ?: "TzTool" + out.printLine( + "Usage: + $main [options] + Options: + -gen Generate fan_tz.js + -embed Generate code to be embedded directly in sys.js + -outDir (optional) generate fan_tz.js in this directory + -verbose, -v Enable verbose logging + -silent Suppress all logging + -help, -? Print usage help + ") + Env.cur.exit(1) + } +////////////////////////////////////////////////////////////////////////// +// Main +////////////////////////////////////////////////////////////////////////// + + static Void main() { TzTool().run } +}