diff --git a/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala b/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala index b10848ac42..36389cfaa9 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/JavaScript.scala @@ -78,7 +78,7 @@ class JavaScript(additionalFeatureFlags: List[String] = Nil) extends Compiler[St */ lazy val CompileLSP = CPSTransformed map { case (mainSymbol, mainFile, core, cps) => - TransformerCps.compileLSP(cps, core) + TransformerCps.compileLSP(cps, core, mainSymbol) } private def pretty(stmts: List[js.Stmt]): Document = diff --git a/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala b/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala index 71ead8eaca..cc361ed4a6 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/PrettyPrinter.scala @@ -62,7 +62,10 @@ object PrettyPrinter extends ParenPrettyPrinter { case Let(id, expr) => "let" <+> toDoc(id) <+> "=" <+> toDoc(expr) <> ";" case Destruct(ids, expr) => "const" <+> braces(hsep(ids.map(toDoc), comma)) <+> "=" <+> toDoc(expr) <> ";" case Assign(target, expr) => toDoc(target) <+> "=" <+> toDoc(expr) <> ";" - case Function(name, params, stmts) => "function" <+> toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts map toDoc) + case Function(name, params, stmts, None) => + "function" <+> toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts map toDoc) + case Function(name, params, stmts, Some(docComment)) => toDoc(docComment) <> line <> + "function" <+> toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts map toDoc) case Class(name, methods) => "class" <+> toDoc(name) <+> jsBlock(methods.map(jsMethod)) case If(cond, thn, Block(Nil)) => "if" <+> parens(toDoc(cond)) <+> toDocBlock(thn) case If(cond, thn, els) => "if" <+> parens(toDoc(cond)) <+> toDocBlock(thn) <+> "else" <+> toDocBlock(els) @@ -78,6 +81,12 @@ object PrettyPrinter extends ParenPrettyPrinter { case Switch(sc, branches, default) => "switch" <+> parens(toDoc(sc)) <+> jsBlock(branches.map { case (tag, stmts) => "case" <+> toDoc(tag) <> ":" <+> nested(stmts map toDoc) } ++ default.toList.map { stmts => "default:" <+> nested(stmts map toDoc) }) + + case LineComment(contents) => "//" <+> contents + case DocComment(lines) => + "/**" <> line <> + vcat(lines.map { l => " *" <+> l }) <> line <> + " */" } def toDocBlock(stmt: Stmt): Doc = stmt match { @@ -87,7 +96,7 @@ object PrettyPrinter extends ParenPrettyPrinter { } def jsMethod(c: js.Function): Doc = c match { - case js.Function(name, params, stmts) => + case js.Function(name, params, stmts, _docComment) => toDoc(name) <> parens(params map toDoc) <+> jsBlock(stmts.map(toDoc)) } diff --git a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala index 4e07a0a3c2..aab826a4d4 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/TransformerCps.scala @@ -36,6 +36,8 @@ object TransformerCps extends Transformer { directStyle: Option[ContinuationInfo], // the current direct-style metacontinuation metacont: Option[Id], + // the main symbol (entrypoint) + mainSymbol: symbols.TermSymbol, // the original declaration context (used to compile pattern matching) declarations: DeclarationContext, // the usual compiler context @@ -52,9 +54,9 @@ object TransformerCps extends Transformer { js.Return(Call(RUN_TOPLEVEL, nameRef(mainSymbol)))))) given DeclarationContext = new DeclarationContext(coreModule.declarations, coreModule.externs) - toJS(input, exports) + toJS(input, exports, mainSymbol) - def toJS(module: cps.ModuleDecl, exports: List[js.Export])(using D: DeclarationContext, C: Context): js.Module = + def toJS(module: cps.ModuleDecl, exports: List[js.Export], mainSymbol: symbols.TermSymbol)(using D: DeclarationContext, C: Context): js.Module = module match { case cps.ModuleDecl(path, includes, declarations, externs, definitions, _) => given TransformerContext( @@ -63,6 +65,7 @@ object TransformerCps extends Transformer { None, None, None, + mainSymbol, D, C) val name = JSName(jsModuleName(module.path)) @@ -73,7 +76,7 @@ object TransformerCps extends Transformer { js.Module(name, Nil, exports, jsDecls ++ jsExterns ++ stmts) } - def compileLSP(input: cps.ModuleDecl, coreModule: core.ModuleDecl)(using C: Context): List[js.Stmt] = + def compileLSP(input: cps.ModuleDecl, coreModule: core.ModuleDecl, mainSymbol: symbols.TermSymbol)(using C: Context): List[js.Stmt] = val D = new DeclarationContext(coreModule.declarations, coreModule.externs) given TransformerContext( false, @@ -81,14 +84,15 @@ object TransformerCps extends Transformer { None, None, None, + mainSymbol, D, C) input.definitions.map(toJS) - def toJS(d: cps.ToplevelDefinition)(using TransformerContext): js.Stmt = d match { + def toJS(d: cps.ToplevelDefinition)(using C: TransformerContext): js.Stmt = d match { case cps.ToplevelDefinition.Def(id, block) => - js.Const(nameDef(id), requiringThunk { toJS(id, block) }) + js.Const(nameDef(id), requiringThunk { toJS(id, block) }, isMainSymbol = C.mainSymbol == id) case cps.ToplevelDefinition.Val(id, ks, k, binding) => js.Const(nameDef(id), Call(RUN_TOPLEVEL, js.Lambda(List(nameDef(ks), nameDef(k)), toJS(binding).stmts))) case cps.ToplevelDefinition.Let(id, binding) => diff --git a/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala b/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala index a5ba7f216e..d1be279179 100644 --- a/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala +++ b/effekt/shared/src/main/scala/effekt/generator/js/Tree.scala @@ -31,6 +31,7 @@ case class Module(name: JSName, imports: List[Import], exports: List[Export], st * Generates the Javascript module skeleton for whole program compilation */ def commonjs: List[Stmt] = { + val typecheckAnnotation = js.LineComment("@ts-check") val effekt = js.Const(JSName("$effekt"), js.Object()) val importStmts = imports.map { @@ -43,11 +44,12 @@ case class Module(name: JSName, imports: List[Import], exports: List[Export], st js.Destruct(names, js.Call(Variable(JSName("require")), List(JsString(s"./${ file }")))) } + val ignoreTypesOfExport = js.LineComment("@ts-ignore - Universal module pattern for Node/Browser compatibility") val exportStatement = js.Assign(RawExpr(s"(typeof module != \"undefined\" && module !== null ? module : {}).exports = ${name.name}"), js.Object(exports.map { e => e.name -> e.expr }) ) - List(effekt) ++ importStmts ++ stmts ++ List(exportStatement) + List(typecheckAnnotation, effekt) ++ importStmts ++ stmts ++ List(ignoreTypesOfExport, exportStatement) } /** @@ -62,6 +64,7 @@ case class Module(name: JSName, imports: List[Import], exports: List[Export], st * }}} */ def virtual : List[Stmt] = { + val typecheckAnnotation = js.LineComment("@ts-check") val effekt = js.Const(JSName("$effekt"), js.Object()) val importStmts = imports.map { @@ -78,7 +81,7 @@ case class Module(name: JSName, imports: List[Import], exports: List[Export], st // module.exports = { EXPORTS } val exportStatement = js.Assign(RawExpr("module.exports"), js.Object(exports.map { e => e.name -> e.expr })) - List(effekt) ++ importStmts ++ List(declaration) ++ stmts ++ List(exportStatement) + List(typecheckAnnotation, effekt) ++ importStmts ++ List(declaration) ++ stmts ++ List(exportStatement) } } @@ -160,8 +163,14 @@ enum Stmt { // e.g. switch (sc) { case : ; ...; default: } case Switch(scrutinee: Expr, branches: List[(Expr, List[Stmt])], default: Option[List[Stmt]]) // TODO maybe flatten? - // e.g. function (x, y) { * } - case Function(name: JSName, params: List[JSName], stmts: List[Stmt]) + // e.g. + // ```js + // /** + // * My doc comment + // */ + // function (x, y) { * } + // ``` + case Function(name: JSName, params: List[JSName], stmts: List[Stmt], docComment: Option[Stmt.DocComment] = None) // e.g. class { // (x, y) { * }... @@ -188,6 +197,17 @@ enum Stmt { // e.g. ; case ExprStmt(expr: Expr) + + // e.g. `// This is my comment` + case LineComment(contents: String) + + // e.g. + // + // /** + // * This is my + // * comment + // */ + case DocComment(lines: List[String]) } export Stmt.* @@ -195,10 +215,19 @@ export Stmt.* // Smart constructors // ------------------ -def Const(name: JSName, binding: Expr): Stmt = binding match { - case Expr.Lambda(params, Block(stmts)) => js.Function(name, params, stmts) - case Expr.Lambda(params, stmt) => js.Function(name, params, List(stmt)) - case _ => js.Const(Pattern.Variable(name), binding) +def Const(name: JSName, binding: Expr, isMainSymbol: Boolean = false): Stmt = { + def docCommentFor(params: List[JSName]): Option[DocComment] = Option.when(isMainSymbol) { + params match { + case ks :: k :: Nil => DocComment(List(s"@param {MetaContinuation} ${ks.name}", s"@param {Continuation} ${k.name}")) + case _ => sys error s"Assumed that the JS entrypoint has exactly two params, but found ${params.length} instead" + } + } + + binding match { + case Expr.Lambda(params, Block(stmts)) => js.Function(name, params, stmts, docCommentFor(params)) + case Expr.Lambda(params, stmt) => js.Function(name, params, List(stmt), docCommentFor(params)) + case _ => js.Const(Pattern.Variable(name), binding) + } } def Let(name: JSName, binding: Expr): Stmt = js.Let(Pattern.Variable(name), binding) diff --git a/libraries/js/effekt_builtins.js b/libraries/js/effekt_builtins.js index 18d5964a41..f3a8490f9a 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -1,3 +1,7 @@ +/** + * @param {*} obj + * @returns {string} + */ $effekt.show = function(obj) { if (!!obj && !!obj.__reflect) { const meta = obj.__reflect() @@ -10,6 +14,11 @@ $effekt.show = function(obj) { } } +/** + * @param {*} obj1 + * @param {*} obj2 + * @returns {boolean} + */ $effekt.equals = function(obj1, obj2) { if (!!obj1.__equals) { return obj1.__equals(obj2) @@ -18,12 +27,21 @@ $effekt.equals = function(obj1, obj2) { } } +/** + * @param {*} n1 + * @param {*} n2 + */ function compare$prim(n1, n2) { if (n1 == n2) { return 0; } else if (n1 > n2) { return 1; } else { return -1; } } +/** + * @param {*} obj1 + * @param {*} obj2 + * @returns {-1 | 0 | 1} - -1 if obj1 < obj2, 0 if equal, 1 if obj1 > obj2 + */ $effekt.compare = function(obj1, obj2) { if ($effekt.equals(obj1, obj2)) { return 0; } @@ -50,12 +68,34 @@ $effekt.compare = function(obj1, obj2) { return compare$prim(obj1, obj2); } +/** + * @typedef {Object} Unit + * @property {true} __unit + */ + +/** + * Unit singleton value (Effekt's `()`) + * @type {Unit} + */ +$effekt.unit = { __unit: true }; + +/** + * @param {string} str + * @returns {Unit} + */ $effekt.println = function println$impl(str) { console.log(str); return $effekt.unit; } -$effekt.unit = { __unit: true } - +/** + * Throws an error for incomplete pattern matches + * @throws {Error} + */ $effekt.emptyMatch = function() { throw "empty match" } -$effekt.hole = function(pos) { throw pos + " not implemented yet" } +/** + * Placeholder for unimplemented code + * @param {string} pos - Source position (already formatted) + * @throws {Error} + */ +$effekt.hole = function(pos) { throw pos + " not implemented yet" } \ No newline at end of file diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index bb149178db..62166a748f 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,12 +1,93 @@ +// Type Definitions +// ---------------- + +/** + * @typedef {function(): *} Thunk + */ + +/** + * Prompt ID type for better type on hover + * @typedef {number} Prompt + */ + +/** + * @typedef {Object} Arena + * @property {MemNode<*>} root + * @property {number} generation + * @property {(t: T) => Reference} fresh + * @property {function(): Arena} newRegion + */ + +/** + * @typedef {Object} MetaContinuation + * @property {Prompt} prompt - Continuation prompt ID + * @property {Arena} arena - Memory arena for mutable state + * @property {MetaContinuation|null} rest - Parent continuation stack + * @property {Continuation|null} [stack] - Stack continuation (optional, can be null) + */ + +/** + * @typedef {Object} CapturedContinuation + * @property {Prompt} prompt - Prompt ID + * @property {Arena} arena - Memory arena + * @property {CapturedContinuation|null} rest - Nested captured continuation + * @property {Continuation|null} [stack] - Stack continuation (optional, can be null) + * @property {Snapshot} backup - Arena backup snapshot + */ + +/** + * @callback Continuation + * @param {*} value - Return value + * @param {MetaContinuation} ks - Metacontinuation + * @returns {Thunk} + */ + +/** + * Resume function passed to CAPTURE body - call with a value to resume the continuation + * @callback ResumeFn + * @param {*} value - Value to pass to the continuation + * @returns {*} + */ + +/** + * @template T + * @typedef {Object} Reference + * @property {T} value - Current value + * @property {number} generation - Version number + * @property {Arena} store - Memory store + * @property {function(T): void} set - Update the reference + */ + +/** + * @template T + * @typedef {Object} DiffNode + * @property {Reference} ref + * @property {T} value + * @property {number} generation + * @property {MemNode} root + */ + +/** + * @template T + * @typedef {Object} MemNode + * @property {DiffNode|null} value + */ + +// State Management +// ---------------- + // Complexity of state: // // get: O(1) // set: O(1) // capture: O(1) // restore: O(|write operations since capture|) -const Mem = null + +// Memory sentinel +const Mem = null; function Arena() { + /** @type {Arena} */ const s = { root: { value: Mem }, generation: 0, @@ -40,12 +121,27 @@ function Arena() { return s } +/** + * @typedef {Object} Snapshot + * @property {Arena} store + * @property {MemNode<*>} root + * @property {number} generation + */ + +/** + * @param {Arena} s - Store to snapshot + */ function snapshot(s) { + /** @type {Snapshot} */ const snap = { store: s, root: s.root, generation: s.generation } s.generation = s.generation + 1 return snap } +/** + * @template T + * @param {MemNode} n - Node to reroot + */ function reroot(n) { if (n.value === Mem) return; @@ -61,6 +157,11 @@ function reroot(n) { r.generation = g } +/** + * @param {Arena} store + * @param {Snapshot} snap + * @returns {void} + */ function restore(store, snap) { // linear in the number of modifications... reroot(snap.root) @@ -72,14 +173,27 @@ function restore(store, snap) { // -------------- let _prompt = 1; +/** @type {Continuation} */ const TOPLEVEL_K = (x, ks) => { throw { computationIsDone: true, result: x } } + +/** @type {MetaContinuation} */ const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null } +/** + * @template T + * @param {function(): T} f + */ function THUNK(f) { - f.thunk = true + // Add thunk marker property - cast to any for property assignment + /** @type {*} */(f).thunk = true return f } +/** + * Captures the current continuation and passes it to body. + * @param {function(ResumeFn): (*|Thunk)} body - Takes a resume function, returns a value or thunk + * @returns {function(MetaContinuation, Continuation): *} + */ function CAPTURE(body) { return (ks, k) => { const res = body(x => TRAMPOLINE(() => k(x, ks))) @@ -88,41 +202,77 @@ function CAPTURE(body) { } } -const RETURN = (x, ks) => ks.rest.stack(x, ks.rest) +/** + * @param {*} x + * @param {MetaContinuation} ks + * @returns {Thunk} + */ +const RETURN = (x, ks) => { + // ks.rest and ks.rest.stack are guaranteed non-null in RESET context + const rest = /** @type {MetaContinuation & {stack: Continuation}} */(ks.rest) + return rest.stack(x, rest) +} -// HANDLE(ks, ks, (p, ks, k) => { STMT }) +/** + * @param {function(Prompt, MetaContinuation, Continuation): Thunk} prog + * @param {MetaContinuation} ks + * @param {Continuation} k + * @returns {Thunk} + */ function RESET(prog, ks, k) { - const prompt = _prompt++; + const prompt = /** @type {Prompt} */(_prompt++); + /** @type {MetaContinuation} */ const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - return prog(prompt, { prompt, arena: Arena([]), rest }, RETURN) + return prog(prompt, { prompt, arena: Arena(), rest }, RETURN) } +/** + * @template T + * @param {Prompt} p - prompt ID + * @param {function(CapturedContinuation, MetaContinuation, Continuation): T} body + * @param {MetaContinuation} ks + * @param {Continuation} [k] - can be undefined + * @returns {T} + */ function SHIFT(p, body, ks, k) { - - // TODO avoid constructing this object + // TODO avoid constructing this `meta` object + /** @type {MetaContinuation|null} */ let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } + /** @type {CapturedContinuation|null} */ let cont = null while (!!meta && meta.prompt !== p) { let store = meta.arena cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } - meta = meta.rest + /** @type {MetaContinuation|null} */ + const nextMeta = meta.rest + meta = nextMeta } if (!meta) { throw `Prompt not found ${p}` } // package the prompt itself let store = meta.arena cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } - meta = meta.rest + // meta.rest is non-null because RESET creates prompts with non-null rest + const parentMeta = /** @type {MetaContinuation} */(meta.rest) - const k1 = meta.stack - meta.stack = null - return body(cont, meta, k1) + const k1 = /** @type {Continuation} */(parentMeta.stack) + // Setting stack to null (it's been captured in cont) + parentMeta.stack = null + return body(/** @type {CapturedContinuation} */(cont), parentMeta, k1) } -// Rewind stack `cont` back onto `k` :: `ks` and resume with c +/** + * @param {CapturedContinuation} cont + * @param {function(MetaContinuation, Continuation): *} c + * @param {MetaContinuation} ks + * @param {Continuation} k + * @returns {Thunk} + */ function RESUME(cont, c, ks, k) { + /** @type {MetaContinuation} */ let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } + /** @type {CapturedContinuation|null} */ let toRewind = cont while (!!toRewind) { restore(toRewind.arena, toRewind.backup) @@ -130,22 +280,34 @@ function RESUME(cont, c, ks, k) { toRewind = toRewind.rest } - const k1 = meta.stack // TODO instead copy meta here, like elsewhere? + const k1 = /** @type {Continuation} */(meta.stack) + // Setting stack to null (it's been captured/restored) meta.stack = null return () => c(meta, k1) } +/** + * @template T + * @param {function(MetaContinuation, Continuation): *} comp + * @returns {T} + */ function RUN_TOPLEVEL(comp) { try { let a = comp(TOPLEVEL_KS, TOPLEVEL_K) - while (true) { a = a() } + while (true) { + a = a() + } } catch (e) { if (e.computationIsDone) return e.result else throw e } } -// trampolines the given computation (like RUN_TOPLEVEL, but doesn't provide continuations) +/** + * @template T + * @param {Thunk} comp + * @returns {T} + */ function TRAMPOLINE(comp) { let a = comp; try { @@ -158,12 +320,21 @@ function TRAMPOLINE(comp) { } } -// keeps the current trampoline going and dispatches the given task +/** + * Keep the current trampoline going and dispatch task on current trampoline + * @param {function(MetaContinuation, Continuation): *} task + * @returns {Thunk} + */ function RUN(task) { return () => task(TOPLEVEL_KS, TOPLEVEL_K) } -// aborts the current continuation +/** + * Abort current continuation with value + * @param {*} value - Value to abort with + * @throws {{computationIsDone: boolean, result: *}} + * @returns {never} + */ function ABORT(value) { throw { computationIsDone: true, result: value } } @@ -214,4 +385,4 @@ $effekt.run = RUN * * If a runtime is available, use `$effekt.run`, instead. */ -$effekt.runToplevel = RUN_TOPLEVEL +$effekt.runToplevel = RUN_TOPLEVEL \ No newline at end of file