From 0cfad00e504c1dc55613b5014c85ec0379d1bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 15:11:05 +0100 Subject: [PATCH 1/7] Experiment: use JSDoc types in JS runtime --- .../effekt/generator/js/JavaScript.scala | 2 +- .../effekt/generator/js/PrettyPrinter.scala | 13 +- .../effekt/generator/js/TransformerCps.scala | 14 +- .../main/scala/effekt/generator/js/Tree.scala | 45 ++- libraries/js/effekt_builtins.js | 102 +++-- libraries/js/effekt_runtime.js | 360 +++++++++++------- 6 files changed, 358 insertions(+), 178 deletions(-) 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..4f3f3ff2fd 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -1,45 +1,77 @@ +/** + * @typedef {Object} Unit + * @property {true} __unit + */ + +/** + * Unit singleton value representing void/no value + * @type {Unit} + */ +$effekt.unit = { __unit: true }; + +/** + * Print a line to console + * @param {string} str - String to print + * @returns {Unit} + */ +$effekt.println = function(str) { + console.log(str); + return $effekt.unit; +}; + +/** + * Convert value to display string + * @param {*} obj - Object to show + * @returns {string} + */ $effekt.show = function(obj) { if (!!obj && !!obj.__reflect) { - const meta = obj.__reflect() - return meta.__name + "(" + meta.__data.map($effekt.show).join(", ") + ")" + const meta = obj.__reflect(); + return meta.__name + "(" + meta.__data.map($effekt.show).join(", ") + ")"; } else if (!!obj && obj.__unit) { return "()"; } else { return "" + obj; } -} +}; +/** + * Check equality between two values + * @param {*} obj1 - First value + * @param {*} obj2 - Second value + * @returns {boolean} + */ $effekt.equals = function(obj1, obj2) { if (!!obj1.__equals) { - return obj1.__equals(obj2) + return obj1.__equals(obj2); } else { return (obj1.__unit && obj2.__unit) || (obj1 === obj2); } -} - -function compare$prim(n1, n2) { - if (n1 == n2) { return 0; } - else if (n1 > n2) { return 1; } - else { return -1; } -} +}; +/** + * Compare two values + * @param {*} obj1 - First value + * @param {*} obj2 - Second value + * @returns {number} - -1 if obj1 < obj2, 0 if equal, 1 if obj1 > obj2 + */ $effekt.compare = function(obj1, obj2) { if ($effekt.equals(obj1, obj2)) { return 0; } if (!!obj1 && !!obj2) { if (!!obj1.__reflect && !!obj2.__reflect) { - const tagOrdering = compare$prim(obj1.__tag, obj2.__tag) + const tagOrdering = compare$prim(obj1.__tag, obj2.__tag); if (tagOrdering != 0) { return tagOrdering; } - const meta1 = obj1.__reflect().__data - const meta2 = obj2.__reflect().__data + const meta1 = obj1.__reflect().__data; + const meta2 = obj2.__reflect().__data; - const lengthOrdering = compare$prim(meta1.length, meta2.length) + const lengthOrdering = compare$prim(meta1.length, meta2.length); if (lengthOrdering != 0) { return lengthOrdering; } for (let i = 0; i < meta1.length; i++) { - const contentOrdering = $effekt.compare(meta1[i], meta2[i]) + const contentOrdering = $effekt.compare(meta1[i], meta2[i]); if (contentOrdering != 0) { return contentOrdering; } } @@ -48,14 +80,36 @@ $effekt.compare = function(obj1, obj2) { } return compare$prim(obj1, obj2); -} - -$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} + * @returns {never} + */ +$effekt.emptyMatch = function() { + throw "empty match"; +}; -$effekt.emptyMatch = function() { throw "empty match" } +/** + * Placeholder for unimplemented code + * @param {string} pos - Source position + * @throws {Error} + * @returns {never} + */ +$effekt.hole = function(pos) { + throw pos + " not implemented yet"; +}; -$effekt.hole = function(pos) { throw pos + " not implemented yet" } +/** + * Internal primitive comparison + * @param {number} n1 + * @param {number} n2 + * @returns {number} + * @private + */ +function compare$prim(n1, n2) { + if (n1 == n2) { return 0; } + else if (n1 > n2) { return 1; } + else { return -1; } +} \ No newline at end of file diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index bb149178db..2353166133 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,11 +1,46 @@ -// Complexity of state: -// -// get: O(1) -// set: O(1) -// capture: O(1) -// restore: O(|write operations since capture|) -const Mem = null +/** + * @typedef {Object} MetaContinuation + * @property {number} prompt - Continuation prompt ID + * @property {Object} arena - Memory arena for mutable state + * @property {MetaContinuation|null} rest - Parent continuation stack + * @property {Continuation} [stack] - Stack continuation (optional) + */ + +/** + * @callback Continuation + * @param {*} value - Return value + * @param {MetaContinuation} ks - Metacontinuation + * @returns {*} + */ + +/** + * @typedef {Object} Reference + * @property {*} value - Current value + * @property {number} generation - Version number + * @property {Object} store - Memory store + * @property {function(*): void} set - Update the reference + */ +/** + * @typedef {Object} DiffNode + * @property {Reference} ref + * @property {*} value + * @property {number} generation + * @property {MemNode} root + */ + +/** + * @typedef {Object} MemNode + * @property {DiffNode|null} value + */ + +// Memory sentinel +const Mem = null; + +/** + * Create a new memory arena for isolated state + * @returns {{root: MemNode, generation: number, fresh: function(*): Reference, newRegion: function(): *}} + */ function Arena() { const s = { root: { value: Mem }, @@ -16,202 +51,251 @@ function Arena() { generation: s.generation, store: s, set: (v) => { - const s = r.store - const r_gen = r.generation - const s_gen = s.generation + const s = r.store; + const r_gen = r.generation; + const s_gen = s.generation; if (r_gen == s_gen) { r.value = v; } else { - const root = { value: Mem } - // update store - s.root.value = { ref: r, value: r.value, generation: r_gen, root: root } - s.root = root - r.value = v - r.generation = s_gen + const root = { value: Mem }; + // @ts-ignore - Complex persistent data structure + s.root.value = { ref: r, value: r.value, generation: r_gen, root: root }; + s.root = root; + r.value = v; + r.generation = s_gen; } } }; - return r + return r; }, - // not implemented newRegion: () => s }; - return s + return s; } +/** + * Capture a snapshot of memory state + * @param {Object} s - Store to snapshot + * @returns {{store: Object, root: MemNode, generation: number}} + */ function snapshot(s) { - const snap = { store: s, root: s.root, generation: s.generation } - s.generation = s.generation + 1 - return snap -} - -function reroot(n) { - if (n.value === Mem) return; - - const diff = n.value - const r = diff.ref - const v = diff.value - const g = diff.generation - const n2 = diff.root - reroot(n2) - n.value = Mem - n2.value = { ref: r, value: r.value, generation: r.generation, root: n} - r.value = v - r.generation = g + const snap = { store: s, root: s.root, generation: s.generation }; + s.generation = s.generation + 1; + return snap; } +/** + * Restore memory to a previous snapshot + * @param {Object} store - Current store + * @param {{store: Object, root: MemNode, generation: number}} snap - Snapshot to restore + * @returns {void} + */ function restore(store, snap) { - // linear in the number of modifications... - reroot(snap.root) - store.root = snap.root - store.generation = snap.generation + 1 + reroot(snap.root); + store.root = snap.root; + store.generation = snap.generation + 1; } -// Common Runtime -// -------------- -let _prompt = 1; +/** + * Internal rerooting for persistent state + * @param {MemNode} n - Node to reroot + * @private + */ +function reroot(n) { + if (n.value === Mem) return; -const TOPLEVEL_K = (x, ks) => { throw { computationIsDone: true, result: x } } -const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null } + const diff = n.value; + const r = diff.ref; + const v = diff.value; + const g = diff.generation; + const n2 = diff.root; + reroot(n2); + n.value = Mem; + n2.value = { ref: r, value: r.value, generation: r.generation, root: n}; + r.value = v; + r.generation = g; +} +/** + * Wrap a thunk for lazy evaluation + * @template T + * @param {function(): T} f - Thunk function + * @returns {function(): T} + */ function THUNK(f) { - f.thunk = true - return f + // @ts-ignore - Adding thunk marker property to function + f.thunk = true; + return f; } +/** + * Capture current continuation + * @param {function(function(*): *): (*|function(): *)} body - Function receiving continuation + * @returns {function(MetaContinuation, Continuation): *} + */ function CAPTURE(body) { return (ks, k) => { - const res = body(x => TRAMPOLINE(() => k(x, ks))) - if (res instanceof Function) return res - else throw { computationIsDone: true, result: $effekt.unit } - } + const res = body(x => TRAMPOLINE(() => k(x, ks))); + if (res instanceof Function) return res; + else throw { computationIsDone: true, result: $effekt.unit }; + }; } -const RETURN = (x, ks) => ks.rest.stack(x, ks.rest) +/** + * Return from current continuation + * @param {*} x - Value to return + * @param {MetaContinuation} ks - Current metacontinuation + * @returns {*} + */ +const RETURN = (x, ks) => { + // @ts-ignore - ks.rest guaranteed non-null in RESET context + return ks.rest.stack(x, ks.rest); +}; -// HANDLE(ks, ks, (p, ks, k) => { STMT }) +/** + * Delimit a continuation with a prompt + * @param {function(number, MetaContinuation, Continuation): *} prog - Program + * @param {MetaContinuation} ks - Current metacontinuation + * @param {Continuation} k - Current continuation + * @returns {*} + */ function RESET(prog, ks, k) { const prompt = _prompt++; - const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - return prog(prompt, { prompt, arena: Arena([]), rest }, RETURN) + /** @type {MetaContinuation} */ + const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest }; + return prog(prompt, { prompt, arena: Arena(), rest }, RETURN); } +/** + * Shift control to captured continuation + * @param {number} p - Prompt to shift to + * @param {function(Object, MetaContinuation, Continuation): *} body - Handler + * @param {MetaContinuation} ks - Current metacontinuation + * @param {Continuation|undefined} k - Current continuation + * @returns {*} + */ function SHIFT(p, body, ks, k) { - - // TODO avoid constructing this object - let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - let cont = null + /** @type {{stack: Continuation, prompt: number, arena: Object, rest: MetaContinuation|null}} */ + let meta = { stack: /** @type {Continuation} */(k), prompt: ks.prompt, arena: ks.arena, rest: ks.rest }; + /** @type {Object|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 + let store = meta.arena; + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont }; + /** @type {any} */ + const nextMeta = meta.rest; + meta = nextMeta; } - if (!meta) { throw `Prompt not found ${p}` } + 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 + // Package the prompt itself + let store = meta.arena; + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont }; + /** @type {any} */ + const nextMeta = meta.rest; + meta = nextMeta; - const k1 = meta.stack - meta.stack = null - return body(cont, meta, k1) + const k1 = meta.stack; + // @ts-ignore - Setting to null is intentional + meta.stack = null; + return body(cont, meta, k1); } -// Rewind stack `cont` back onto `k` :: `ks` and resume with c +/** + * Resume a captured continuation + * @param {Object} cont - Captured continuation + * @param {*} c - Value to resume with + * @param {MetaContinuation} ks - Current metacontinuation + * @param {Continuation} k - Current continuation + * @returns {function(): *} + */ function RESUME(cont, c, ks, k) { - let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - let toRewind = cont + /** @type {any} */ + let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest }; + /** @type {any} */ + let toRewind = cont; while (!!toRewind) { - restore(toRewind.arena, toRewind.backup) - meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta } - toRewind = toRewind.rest + restore(toRewind.arena, toRewind.backup); + meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta }; + toRewind = toRewind.rest; } - const k1 = meta.stack // TODO instead copy meta here, like elsewhere? - meta.stack = null - return () => c(meta, k1) + const k1 = meta.stack; + // @ts-ignore - Setting to null is intentional + meta.stack = null; + return () => c(meta, k1); } +/** + * Run computation at top level + * @template T + * @param {function(MetaContinuation, Continuation): (function(): *)} comp - Computation + * @returns {T} + */ function RUN_TOPLEVEL(comp) { try { - let a = comp(TOPLEVEL_KS, TOPLEVEL_K) - while (true) { a = a() } + let a = comp(TOPLEVEL_KS, TOPLEVEL_K); + while (true) { + a = a(); + } } catch (e) { - if (e.computationIsDone) return e.result - else throw e + if (e.computationIsDone) return e.result; + else throw e; } } -// trampolines the given computation (like RUN_TOPLEVEL, but doesn't provide continuations) +/** + * Trampoline a computation + * @template T + * @param {function(): (T|function(): *)} comp - Computation + * @returns {T} + */ function TRAMPOLINE(comp) { let a = comp; try { while (true) { - a = a() + // @ts-ignore - Dynamic trampolining + a = a(); } } catch (e) { - if (e.computationIsDone) return e.result - else throw e + if (e.computationIsDone) return e.result; + else throw e; } } -// keeps the current trampoline going and dispatches the given task +/** + * Dispatch task on current trampoline + * @param {function(MetaContinuation, Continuation): *} task - Task + * @returns {function(): *} + */ function RUN(task) { - return () => task(TOPLEVEL_KS, TOPLEVEL_K) + 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 } + throw { computationIsDone: true, result: value }; } +// Public API exports +$effekt.capture = CAPTURE; +$effekt.run = RUN; +$effekt.runToplevel = RUN_TOPLEVEL; + +// Internal constants +let _prompt = 1; + +/** @type {Continuation} */ +const TOPLEVEL_K = (x, ks) => { + throw { computationIsDone: true, result: x }; +}; -// "Public API" used in FFI -// ------------------------ - -/** - * Captures the current continuation as a WHOLE and makes it available - * as argument to the passed body. For example: - * - * $effekt.capture(k => ... k(42) ...) - * - * The body - * - * $effekt.capture(k => >>> ... <<<) - * - * conceptually runs on the _native_ JS call stack. You can call JS functions, - * like `setTimeout` etc., but you are not expected or required to return an - * Effekt value like `$effekt.unit`. If you want to run an Effekt computation - * like in `io::spawn`, you can use `$effekt.run`. - * - * Advanced usage details: - * - * The value returned by the function passed to `capture` returns - * - a function: the returned function will be passed to the - * Effekt runtime, which performs trampolining. - * In this case, the Effekt runtime will keep running, though the - * continuation has been removed. - * - * - another value (like `undefined`): the Effekt runtime will terminate. - */ -$effekt.capture = CAPTURE - -/** - * Used to call Effekt function arguments in the JS FFI, like in `io::spawn`. - * - * Requires an active Effekt runtime (trampoline). - */ -$effekt.run = RUN - -/** - * Used to call Effekt function arguments in the JS FFI, like in `network::listen`. - * - * This function should be used when _no_ Effekt runtime is available. For instance, - * in callbacks passed to the NodeJS eventloop. - * - * If a runtime is available, use `$effekt.run`, instead. - */ -$effekt.runToplevel = RUN_TOPLEVEL +/** @type {MetaContinuation} */ +const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null }; \ No newline at end of file From 5c5bd14b787212d941614b6513ceed51d35d91db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 15:24:53 +0100 Subject: [PATCH 2/7] Try to minimize diff --- libraries/js/effekt_builtins.js | 30 ++-- libraries/js/effekt_runtime.js | 243 ++++++++++++++++++-------------- 2 files changed, 154 insertions(+), 119 deletions(-) diff --git a/libraries/js/effekt_builtins.js b/libraries/js/effekt_builtins.js index 4f3f3ff2fd..76b5e65f4f 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -26,15 +26,15 @@ $effekt.println = function(str) { */ $effekt.show = function(obj) { if (!!obj && !!obj.__reflect) { - const meta = obj.__reflect(); - return meta.__name + "(" + meta.__data.map($effekt.show).join(", ") + ")"; + const meta = obj.__reflect() + return meta.__name + "(" + meta.__data.map($effekt.show).join(", ") + ")" } else if (!!obj && obj.__unit) { return "()"; } else { return "" + obj; } -}; +} /** * Check equality between two values @@ -44,11 +44,11 @@ $effekt.show = function(obj) { */ $effekt.equals = function(obj1, obj2) { if (!!obj1.__equals) { - return obj1.__equals(obj2); + return obj1.__equals(obj2) } else { return (obj1.__unit && obj2.__unit) || (obj1 === obj2); } -}; +} /** * Compare two values @@ -61,17 +61,17 @@ $effekt.compare = function(obj1, obj2) { if (!!obj1 && !!obj2) { if (!!obj1.__reflect && !!obj2.__reflect) { - const tagOrdering = compare$prim(obj1.__tag, obj2.__tag); + const tagOrdering = compare$prim(obj1.__tag, obj2.__tag) if (tagOrdering != 0) { return tagOrdering; } - const meta1 = obj1.__reflect().__data; - const meta2 = obj2.__reflect().__data; + const meta1 = obj1.__reflect().__data + const meta2 = obj2.__reflect().__data - const lengthOrdering = compare$prim(meta1.length, meta2.length); + const lengthOrdering = compare$prim(meta1.length, meta2.length) if (lengthOrdering != 0) { return lengthOrdering; } for (let i = 0; i < meta1.length; i++) { - const contentOrdering = $effekt.compare(meta1[i], meta2[i]); + const contentOrdering = $effekt.compare(meta1[i], meta2[i]) if (contentOrdering != 0) { return contentOrdering; } } @@ -80,7 +80,7 @@ $effekt.compare = function(obj1, obj2) { } return compare$prim(obj1, obj2); -}; +} /** * Throws an error for incomplete pattern matches @@ -88,8 +88,8 @@ $effekt.compare = function(obj1, obj2) { * @returns {never} */ $effekt.emptyMatch = function() { - throw "empty match"; -}; + throw "empty match" +} /** * Placeholder for unimplemented code @@ -98,8 +98,8 @@ $effekt.emptyMatch = function() { * @returns {never} */ $effekt.hole = function(pos) { - throw pos + " not implemented yet"; -}; + throw pos + " not implemented yet" +} /** * Internal primitive comparison diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index 2353166133..3396825a0d 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -51,70 +51,70 @@ function Arena() { generation: s.generation, store: s, set: (v) => { - const s = r.store; - const r_gen = r.generation; - const s_gen = s.generation; + const s = r.store + const r_gen = r.generation + const s_gen = s.generation if (r_gen == s_gen) { r.value = v; } else { - const root = { value: Mem }; - // @ts-ignore - Complex persistent data structure - s.root.value = { ref: r, value: r.value, generation: r_gen, root: root }; - s.root = root; - r.value = v; - r.generation = s_gen; + const root = { value: Mem } + // update store + + // @ts-ignore - Setting up diff node + s.root.value = { ref: r, value: r.value, generation: r_gen, root: root } + s.root = root + r.value = v + r.generation = s_gen } } }; - return r; + return r }, + // not implemented newRegion: () => s }; - return s; + return s } /** - * Capture a snapshot of memory state * @param {Object} s - Store to snapshot * @returns {{store: Object, root: MemNode, generation: number}} */ function snapshot(s) { - const snap = { store: s, root: s.root, generation: s.generation }; - s.generation = s.generation + 1; - return snap; + const snap = { store: s, root: s.root, generation: s.generation } + s.generation = s.generation + 1 + return snap } /** - * Restore memory to a previous snapshot - * @param {Object} store - Current store - * @param {{store: Object, root: MemNode, generation: number}} snap - Snapshot to restore + * @param {Object} store + * @param {{store: Object, root: MemNode, generation: number}} snap * @returns {void} */ function restore(store, snap) { - reroot(snap.root); - store.root = snap.root; - store.generation = snap.generation + 1; + reroot(snap.root) + store.root = snap.root + store.generation = snap.generation + 1 } /** - * Internal rerooting for persistent state * @param {MemNode} n - Node to reroot * @private */ function reroot(n) { if (n.value === Mem) return; - const diff = n.value; - const r = diff.ref; - const v = diff.value; - const g = diff.generation; - const n2 = diff.root; - reroot(n2); - n.value = Mem; - n2.value = { ref: r, value: r.value, generation: r.generation, root: n}; - r.value = v; - r.generation = g; + const diff = n.value + const r = diff.ref + const v = diff.value + const g = diff.generation + const n2 = diff.root + reroot(n2) + n.value = Mem + n2.value = { ref: r, value: r.value, generation: r.generation, root: n } + r.value = v + r.generation = g } /** @@ -125,13 +125,12 @@ function reroot(n) { */ function THUNK(f) { // @ts-ignore - Adding thunk marker property to function - f.thunk = true; - return f; + f.thunk = true + return f } /** - * Capture current continuation - * @param {function(function(*): *): (*|function(): *)} body - Function receiving continuation + * @param {function(function(*): *): (*|function(): *)} body * @returns {function(MetaContinuation, Continuation): *} */ function CAPTURE(body) { @@ -139,117 +138,110 @@ function CAPTURE(body) { const res = body(x => TRAMPOLINE(() => k(x, ks))); if (res instanceof Function) return res; else throw { computationIsDone: true, result: $effekt.unit }; - }; + } } /** - * Return from current continuation - * @param {*} x - Value to return - * @param {MetaContinuation} ks - Current metacontinuation + * @param {*} x + * @param {MetaContinuation} ks * @returns {*} */ const RETURN = (x, ks) => { - // @ts-ignore - ks.rest guaranteed non-null in RESET context - return ks.rest.stack(x, ks.rest); -}; + // @ts-ignore - ks.rest guaranteed non-null in RESET context [?] + return ks.rest.stack(x, ks.rest) +} /** - * Delimit a continuation with a prompt - * @param {function(number, MetaContinuation, Continuation): *} prog - Program - * @param {MetaContinuation} ks - Current metacontinuation - * @param {Continuation} k - Current continuation + * @param {function(number, MetaContinuation, Continuation): *} prog + * @param {MetaContinuation} ks + * @param {Continuation} k * @returns {*} */ function RESET(prog, ks, k) { const 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); + const rest = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } + return prog(prompt, { prompt, arena: Arena(), rest }, RETURN) } /** - * Shift control to captured continuation - * @param {number} p - Prompt to shift to - * @param {function(Object, MetaContinuation, Continuation): *} body - Handler - * @param {MetaContinuation} ks - Current metacontinuation - * @param {Continuation|undefined} k - Current continuation + * @param {number} p - prompt ID + * @param {function(Object, MetaContinuation, Continuation): *} body + * @param {MetaContinuation} ks + * @param {Continuation|undefined} k * @returns {*} */ function SHIFT(p, body, ks, k) { + // TODO avoid constructing this `meta` object /** @type {{stack: Continuation, prompt: number, arena: Object, rest: MetaContinuation|null}} */ - let meta = { stack: /** @type {Continuation} */(k), prompt: ks.prompt, arena: ks.arena, rest: ks.rest }; + let meta = { stack: /** @type {Continuation} */(k), prompt: ks.prompt, arena: ks.arena, rest: ks.rest } /** @type {Object|null} */ - let cont = 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 }; + let store = meta.arena + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } /** @type {any} */ - const nextMeta = meta.rest; - meta = nextMeta; + const nextMeta = meta.rest + meta = nextMeta } - if (!meta) { throw `Prompt not found ${p}`; } + 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 }; + let store = meta.arena + cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } /** @type {any} */ - const nextMeta = meta.rest; - meta = nextMeta; + const nextMeta = meta.rest + meta = nextMeta - const k1 = meta.stack; + const k1 = meta.stack // @ts-ignore - Setting to null is intentional - meta.stack = null; - return body(cont, meta, k1); + meta.stack = null + return body(cont, meta, k1) } /** - * Resume a captured continuation - * @param {Object} cont - Captured continuation - * @param {*} c - Value to resume with - * @param {MetaContinuation} ks - Current metacontinuation - * @param {Continuation} k - Current continuation + * @param {Object} cont + * @param {*} c + * @param {MetaContinuation} ks + * @param {Continuation} k * @returns {function(): *} */ function RESUME(cont, c, ks, k) { /** @type {any} */ - let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest }; + let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } /** @type {any} */ - let toRewind = cont; + let toRewind = cont while (!!toRewind) { - restore(toRewind.arena, toRewind.backup); - meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta }; - toRewind = toRewind.rest; + restore(toRewind.arena, toRewind.backup) + meta = { stack: toRewind.stack, prompt: toRewind.prompt, arena: toRewind.arena, rest: meta } + toRewind = toRewind.rest } - const k1 = meta.stack; + const k1 = meta.stack; // TODO instead copy `meta` here, like elsewhere? // @ts-ignore - Setting to null is intentional - meta.stack = null; - return () => c(meta, k1); + meta.stack = null + return () => c(meta, k1) } /** - * Run computation at top level * @template T - * @param {function(MetaContinuation, Continuation): (function(): *)} comp - Computation + * @param {function(MetaContinuation, Continuation): (function(): *)} comp * @returns {T} */ function RUN_TOPLEVEL(comp) { try { - let a = comp(TOPLEVEL_KS, TOPLEVEL_K); - while (true) { - a = a(); - } + let a = comp(TOPLEVEL_KS, TOPLEVEL_K) + while (true) { a = a() } } catch (e) { - if (e.computationIsDone) return e.result; - else throw e; + if (e.computationIsDone) return e.result + else throw e } } /** - * Trampoline a computation * @template T - * @param {function(): (T|function(): *)} comp - Computation + * @param {function(): (T|function(): *)} comp * @returns {T} */ function TRAMPOLINE(comp) { @@ -257,21 +249,21 @@ function TRAMPOLINE(comp) { try { while (true) { // @ts-ignore - Dynamic trampolining - a = a(); + a = a() } } catch (e) { - if (e.computationIsDone) return e.result; - else throw e; + if (e.computationIsDone) return e.result + else throw e } } /** - * Dispatch task on current trampoline - * @param {function(MetaContinuation, Continuation): *} task - Task + * Keep the current trampoline going and dispatch task on current trampoline + * @param {function(MetaContinuation, Continuation): *} task * @returns {function(): *} */ function RUN(task) { - return () => task(TOPLEVEL_KS, TOPLEVEL_K); + return () => task(TOPLEVEL_KS, TOPLEVEL_K) } /** @@ -281,14 +273,9 @@ function RUN(task) { * @returns {never} */ function ABORT(value) { - throw { computationIsDone: true, result: value }; + throw { computationIsDone: true, result: value } } -// Public API exports -$effekt.capture = CAPTURE; -$effekt.run = RUN; -$effekt.runToplevel = RUN_TOPLEVEL; - // Internal constants let _prompt = 1; @@ -298,4 +285,52 @@ const TOPLEVEL_K = (x, ks) => { }; /** @type {MetaContinuation} */ -const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null }; \ No newline at end of file +const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null }; + +// "Public API" used in FFI +// ------------------------ + +/** + * Captures the current continuation as a WHOLE and makes it available + * as argument to the passed body. For example: + * + * $effekt.capture(k => ... k(42) ...) + * + * The body + * + * $effekt.capture(k => >>> ... <<<) + * + * conceptually runs on the _native_ JS call stack. You can call JS functions, + * like `setTimeout` etc., but you are not expected or required to return an + * Effekt value like `$effekt.unit`. If you want to run an Effekt computation + * like in `io::spawn`, you can use `$effekt.run`. + * + * Advanced usage details: + * + * The value returned by the function passed to `capture` returns + * - a function: the returned function will be passed to the + * Effekt runtime, which performs trampolining. + * In this case, the Effekt runtime will keep running, though the + * continuation has been removed. + * + * - another value (like `undefined`): the Effekt runtime will terminate. + */ +$effekt.capture = CAPTURE; + + +/** + * Used to call Effekt function arguments in the JS FFI, like in `io::spawn`. + * + * Requires an active Effekt runtime (trampoline). + */ +$effekt.run = RUN; + +/** + * Used to call Effekt function arguments in the JS FFI, like in `network::listen`. + * + * This function should be used when _no_ Effekt runtime is available. For instance, + * in callbacks passed to the NodeJS eventloop. + * + * If a runtime is available, use `$effekt.run`, instead. + */ +$effekt.runToplevel = RUN_TOPLEVEL; \ No newline at end of file From f2f81e94df55e9a117c33b563c946979b476fcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 15:31:35 +0100 Subject: [PATCH 3/7] Minimize diff further, remove trivial comments --- libraries/js/effekt_builtins.js | 83 +++++++++++++++------------------ libraries/js/effekt_runtime.js | 62 ++++++++++++++---------- 2 files changed, 74 insertions(+), 71 deletions(-) diff --git a/libraries/js/effekt_builtins.js b/libraries/js/effekt_builtins.js index 76b5e65f4f..bef9904eaf 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -1,27 +1,5 @@ /** - * @typedef {Object} Unit - * @property {true} __unit - */ - -/** - * Unit singleton value representing void/no value - * @type {Unit} - */ -$effekt.unit = { __unit: true }; - -/** - * Print a line to console - * @param {string} str - String to print - * @returns {Unit} - */ -$effekt.println = function(str) { - console.log(str); - return $effekt.unit; -}; - -/** - * Convert value to display string - * @param {*} obj - Object to show + * @param {*} obj * @returns {string} */ $effekt.show = function(obj) { @@ -37,9 +15,8 @@ $effekt.show = function(obj) { } /** - * Check equality between two values - * @param {*} obj1 - First value - * @param {*} obj2 - Second value + * @param {*} obj1 + * @param {*} obj2 * @returns {boolean} */ $effekt.equals = function(obj1, obj2) { @@ -51,10 +28,19 @@ $effekt.equals = function(obj1, obj2) { } /** - * Compare two values - * @param {*} obj1 - First value - * @param {*} obj2 - Second value - * @returns {number} - -1 if obj1 < obj2, 0 if equal, 1 if obj1 > obj2 + * @param {number} n1 + * @param {number} 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; } @@ -82,10 +68,29 @@ $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(str) { + console.log(str); + return $effekt.unit; +}; + /** * Throws an error for incomplete pattern matches * @throws {Error} - * @returns {never} */ $effekt.emptyMatch = function() { throw "empty match" @@ -93,23 +98,9 @@ $effekt.emptyMatch = function() { /** * Placeholder for unimplemented code - * @param {string} pos - Source position + * @param {string} pos - Source position (already formatted) * @throws {Error} - * @returns {never} */ $effekt.hole = function(pos) { throw pos + " not implemented yet" -} - -/** - * Internal primitive comparison - * @param {number} n1 - * @param {number} n2 - * @returns {number} - * @private - */ -function compare$prim(n1, n2) { - if (n1 == n2) { return 0; } - else if (n1 > n2) { return 1; } - else { return -1; } } \ No newline at end of file diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index 3396825a0d..8c2c167f83 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,3 +1,6 @@ +// Type Definitions +// ---------------- + /** * @typedef {Object} MetaContinuation * @property {number} prompt - Continuation prompt ID @@ -34,11 +37,20 @@ * @property {DiffNode|null} value */ +// State Management +// ---------------- + +// Complexity of state: +// +// get: O(1) +// set: O(1) +// capture: O(1) +// restore: O(|write operations since capture|) + // Memory sentinel const Mem = null; /** - * Create a new memory arena for isolated state * @returns {{root: MemNode, generation: number, fresh: function(*): Reference, newRegion: function(): *}} */ function Arena() { @@ -87,17 +99,6 @@ function snapshot(s) { return snap } -/** - * @param {Object} store - * @param {{store: Object, root: MemNode, generation: number}} snap - * @returns {void} - */ -function restore(store, snap) { - reroot(snap.root) - store.root = snap.root - store.generation = snap.generation + 1 -} - /** * @param {MemNode} n - Node to reroot * @private @@ -118,9 +119,31 @@ function reroot(n) { } /** - * Wrap a thunk for lazy evaluation + * @param {Object} store + * @param {{store: Object, root: MemNode, generation: number}} snap + * @returns {void} + */ +function restore(store, snap) { + // linear in the number of modifications... + reroot(snap.root) + store.root = snap.root + store.generation = snap.generation + 1 +} + +// Common Runtime +// -------------- + +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 - Thunk function + * @param {function(): T} f * @returns {function(): T} */ function THUNK(f) { @@ -276,17 +299,6 @@ function ABORT(value) { throw { computationIsDone: true, result: value } } -// Internal constants -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 }; - // "Public API" used in FFI // ------------------------ From d8722520ada027770d862bfbcca48b915f4c8e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 15:34:37 +0100 Subject: [PATCH 4/7] Continue minimizing diff --- libraries/js/effekt_runtime.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index 8c2c167f83..90377dd7ee 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -113,7 +113,7 @@ function reroot(n) { const n2 = diff.root reroot(n2) n.value = Mem - n2.value = { ref: r, value: r.value, generation: r.generation, root: n } + n2.value = { ref: r, value: r.value, generation: r.generation, root: n} r.value = v r.generation = g } @@ -132,7 +132,6 @@ function restore(store, snap) { // Common Runtime // -------------- - let _prompt = 1; /** @type {Continuation} */ @@ -158,9 +157,9 @@ function THUNK(f) { */ function CAPTURE(body) { return (ks, k) => { - const res = body(x => TRAMPOLINE(() => k(x, ks))); - if (res instanceof Function) return res; - else throw { computationIsDone: true, result: $effekt.unit }; + const res = body(x => TRAMPOLINE(() => k(x, ks))) + if (res instanceof Function) return res + else throw { computationIsDone: true, result: $effekt.unit } } } @@ -210,7 +209,7 @@ function SHIFT(p, body, ks, k) { } if (!meta) { throw `Prompt not found ${p}` } - // Package the prompt itself + // package the prompt itself let store = meta.arena cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } /** @type {any} */ @@ -241,7 +240,7 @@ function RESUME(cont, c, ks, k) { toRewind = toRewind.rest } - const k1 = meta.stack; // TODO instead copy `meta` here, like elsewhere? + const k1 = meta.stack // TODO instead copy `meta` here, like elsewhere? // @ts-ignore - Setting to null is intentional meta.stack = null return () => c(meta, k1) @@ -299,6 +298,7 @@ function ABORT(value) { throw { computationIsDone: true, result: value } } + // "Public API" used in FFI // ------------------------ @@ -327,15 +327,14 @@ function ABORT(value) { * * - another value (like `undefined`): the Effekt runtime will terminate. */ -$effekt.capture = CAPTURE; - +$effekt.capture = CAPTURE /** * Used to call Effekt function arguments in the JS FFI, like in `io::spawn`. * * Requires an active Effekt runtime (trampoline). */ -$effekt.run = RUN; +$effekt.run = RUN /** * Used to call Effekt function arguments in the JS FFI, like in `network::listen`. @@ -345,4 +344,4 @@ $effekt.run = RUN; * * If a runtime is available, use `$effekt.run`, instead. */ -$effekt.runToplevel = RUN_TOPLEVEL; \ No newline at end of file +$effekt.runToplevel = RUN_TOPLEVEL \ No newline at end of file From 010392c8d767422fdf513d1dde2c36e1df277751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 17:53:50 +0100 Subject: [PATCH 5/7] Try using tighter type annotations --- libraries/js/effekt_runtime.js | 157 +++++++++++++++++++++------------ 1 file changed, 99 insertions(+), 58 deletions(-) diff --git a/libraries/js/effekt_runtime.js b/libraries/js/effekt_runtime.js index 90377dd7ee..62166a748f 100644 --- a/libraries/js/effekt_runtime.js +++ b/libraries/js/effekt_runtime.js @@ -1,40 +1,76 @@ // 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 {number} prompt - Continuation prompt ID - * @property {Object} arena - Memory arena for mutable state + * @property {Prompt} prompt - Continuation prompt ID + * @property {Arena} arena - Memory arena for mutable state * @property {MetaContinuation|null} rest - Parent continuation stack - * @property {Continuation} [stack] - Stack continuation (optional) + * @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 {*} value - Current value + * @property {T} value - Current value * @property {number} generation - Version number - * @property {Object} store - Memory store - * @property {function(*): void} set - Update the reference + * @property {Arena} store - Memory store + * @property {function(T): void} set - Update the reference */ /** + * @template T * @typedef {Object} DiffNode - * @property {Reference} ref - * @property {*} value + * @property {Reference} ref + * @property {T} value * @property {number} generation - * @property {MemNode} root + * @property {MemNode} root */ /** + * @template T * @typedef {Object} MemNode - * @property {DiffNode|null} value + * @property {DiffNode|null} value */ // State Management @@ -50,10 +86,8 @@ // Memory sentinel const Mem = null; -/** - * @returns {{root: MemNode, generation: number, fresh: function(*): Reference, newRegion: function(): *}} - */ function Arena() { + /** @type {Arena} */ const s = { root: { value: Mem }, generation: 0, @@ -72,8 +106,6 @@ function Arena() { } else { const root = { value: Mem } // update store - - // @ts-ignore - Setting up diff node s.root.value = { ref: r, value: r.value, generation: r_gen, root: root } s.root = root r.value = v @@ -90,18 +122,25 @@ function Arena() { } /** - * @param {Object} s - Store to snapshot - * @returns {{store: Object, root: MemNode, generation: number}} + * @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 } /** - * @param {MemNode} n - Node to reroot - * @private + * @template T + * @param {MemNode} n - Node to reroot */ function reroot(n) { if (n.value === Mem) return; @@ -119,8 +158,8 @@ function reroot(n) { } /** - * @param {Object} store - * @param {{store: Object, root: MemNode, generation: number}} snap + * @param {Arena} store + * @param {Snapshot} snap * @returns {void} */ function restore(store, snap) { @@ -143,16 +182,16 @@ const TOPLEVEL_KS = { prompt: 0, arena: Arena(), rest: null } /** * @template T * @param {function(): T} f - * @returns {function(): T} */ function THUNK(f) { - // @ts-ignore - Adding thunk marker property to function - f.thunk = true + // Add thunk marker property - cast to any for property assignment + /** @type {*} */(f).thunk = true return f } /** - * @param {function(function(*): *): (*|function(): *)} body + * 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) { @@ -166,44 +205,46 @@ function CAPTURE(body) { /** * @param {*} x * @param {MetaContinuation} ks - * @returns {*} + * @returns {Thunk} */ const RETURN = (x, ks) => { - // @ts-ignore - ks.rest guaranteed non-null in RESET context [?] - return ks.rest.stack(x, ks.rest) + // 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) } /** - * @param {function(number, MetaContinuation, Continuation): *} prog + * @param {function(Prompt, MetaContinuation, Continuation): Thunk} prog * @param {MetaContinuation} ks * @param {Continuation} k - * @returns {*} + * @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) } /** - * @param {number} p - prompt ID - * @param {function(Object, MetaContinuation, Continuation): *} body + * @template T + * @param {Prompt} p - prompt ID + * @param {function(CapturedContinuation, MetaContinuation, Continuation): T} body * @param {MetaContinuation} ks - * @param {Continuation|undefined} k - * @returns {*} + * @param {Continuation} [k] - can be undefined + * @returns {T} */ function SHIFT(p, body, ks, k) { // TODO avoid constructing this `meta` object - /** @type {{stack: Continuation, prompt: number, arena: Object, rest: MetaContinuation|null}} */ - let meta = { stack: /** @type {Continuation} */(k), prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - /** @type {Object|null} */ + /** @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 } - /** @type {any} */ + /** @type {MetaContinuation|null} */ const nextMeta = meta.rest meta = nextMeta } @@ -212,27 +253,26 @@ function SHIFT(p, body, ks, k) { // package the prompt itself let store = meta.arena cont = { stack: meta.stack, prompt: meta.prompt, arena: store, backup: snapshot(store), rest: cont } - /** @type {any} */ - const nextMeta = meta.rest - meta = nextMeta + // meta.rest is non-null because RESET creates prompts with non-null rest + const parentMeta = /** @type {MetaContinuation} */(meta.rest) - const k1 = meta.stack - // @ts-ignore - Setting to null is intentional - 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) } /** - * @param {Object} cont - * @param {*} c + * @param {CapturedContinuation} cont + * @param {function(MetaContinuation, Continuation): *} c * @param {MetaContinuation} ks * @param {Continuation} k - * @returns {function(): *} + * @returns {Thunk} */ function RESUME(cont, c, ks, k) { - /** @type {any} */ + /** @type {MetaContinuation} */ let meta = { stack: k, prompt: ks.prompt, arena: ks.arena, rest: ks.rest } - /** @type {any} */ + /** @type {CapturedContinuation|null} */ let toRewind = cont while (!!toRewind) { restore(toRewind.arena, toRewind.backup) @@ -240,21 +280,23 @@ function RESUME(cont, c, ks, k) { toRewind = toRewind.rest } - const k1 = meta.stack // TODO instead copy `meta` here, like elsewhere? - // @ts-ignore - Setting to null is intentional + 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): (function(): *)} comp + * @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 @@ -263,14 +305,13 @@ function RUN_TOPLEVEL(comp) { /** * @template T - * @param {function(): (T|function(): *)} comp + * @param {Thunk} comp * @returns {T} */ function TRAMPOLINE(comp) { let a = comp; try { while (true) { - // @ts-ignore - Dynamic trampolining a = a() } } catch (e) { @@ -282,7 +323,7 @@ function TRAMPOLINE(comp) { /** * Keep the current trampoline going and dispatch task on current trampoline * @param {function(MetaContinuation, Continuation): *} task - * @returns {function(): *} + * @returns {Thunk} */ function RUN(task) { return () => task(TOPLEVEL_KS, TOPLEVEL_K) From ce57b90eb46117cdc6c5ea54ff1b317a78e71dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 17:58:44 +0100 Subject: [PATCH 6/7] Minimize diff even futher --- libraries/js/effekt_builtins.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/libraries/js/effekt_builtins.js b/libraries/js/effekt_builtins.js index bef9904eaf..68b201eac0 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -83,24 +83,19 @@ $effekt.unit = { __unit: true }; * @param {string} str * @returns {Unit} */ -$effekt.println = function(str) { - console.log(str); - return $effekt.unit; -}; +$effekt.println = function println$impl(str) { + console.log(str); return $effekt.unit; +} /** * Throws an error for incomplete pattern matches * @throws {Error} */ -$effekt.emptyMatch = function() { - throw "empty match" -} +$effekt.emptyMatch = function() { throw "empty match" } /** * 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 +$effekt.hole = function(pos) { throw pos + " not implemented yet" } \ No newline at end of file From a8e7108b79e31666c05d3b6bf40f0bf04a5b7019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Bene=C5=A1?= Date: Wed, 19 Nov 2025 17:59:59 +0100 Subject: [PATCH 7/7] Primitive comparison should work for more types than numbers --- libraries/js/effekt_builtins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/js/effekt_builtins.js b/libraries/js/effekt_builtins.js index 68b201eac0..f3a8490f9a 100644 --- a/libraries/js/effekt_builtins.js +++ b/libraries/js/effekt_builtins.js @@ -28,8 +28,8 @@ $effekt.equals = function(obj1, obj2) { } /** - * @param {number} n1 - * @param {number} n2 + * @param {*} n1 + * @param {*} n2 */ function compare$prim(n1, n2) { if (n1 == n2) { return 0; }