@fraczak/k 6.9.0 → 6.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
  This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.10.1] — 2026-10-02 — Fix Compaction Mark Reset and False Tail Calls in LLVM & WASM Backends
9
+
10
+ ### LLVM & WebAssembly Backends
11
+
12
+ - **Compaction Mark Reset**: Fixed an issue in both LLVM (`krt.c`) and WebAssembly (`kvm2wasm.mjs`) where tail-loop compaction never reset the baseline arena mark after compacting live objects. Once the threshold was reached, compaction was erroneously triggered on every subsequent iteration, turning $O(N)$ execution into $O(N^2)$ and causing call-stack overflow on large inputs.
13
+ - **Product Branch Tail-Call Fix**: Fixed LLVM lowering in `kvm2llvm.mjs` where recursive calls inside product constructor fields were incorrectly classified as self-tail calls.
14
+
15
+ ---
16
+
17
+ ## [6.10.0] — 2026-10-02 — Dual Evaluation Engines (WASM & JS) and REPL Command Modernization
18
+
19
+ ### REPL & Evaluation Engines
20
+
21
+ - **Dual Evaluation Engines**: The REPL now supports two evaluation modes:
22
+ - `wasm` (default): Lowers expressions via polymorphic kVM to WebAssembly AOT bytecode for near-native performance, tail-call optimization, and stack safety.
23
+ - `js`: Direct tree-walking JavaScript interpreter evaluating parsed relations via `run_rel` with pattern constraints for instant interpretation without compilation latency.
24
+ - **Engine Switching & CLI**:
25
+ - REPL commands `:engine [wasm|js]`, `:wasm`, and `:js` dynamically inspect or switch the active evaluation engine.
26
+ - CLI options `--engine=wasm`, `--engine=js`, `--wasm`, and `--js` added to `k-repl`.
27
+ - Active engine displayed in execution timing annotations (`/* comp: Xms, exec: Yms (wasm) */` or `(js)`).
28
+ - **Web REPL (`repl.html`) Integration**:
29
+ - Added clickable navbar engine badge (`wasm-in-process` <-> `js-in-process`) for one-click engine toggling in the browser.
30
+ - Exported `getEngine()` and `setEngine()` on `window.kRepl`.
31
+ - **Command Modernization**:
32
+ - Removed deprecated REPL commands: `:run expr`, `:eval expr`, `:time expr`, and `:t name`.
33
+ - Renamed `:codes` to `:types` (listing type aliases).
34
+ - Renamed `:d name` to `:rel name` (displaying relation definitions).
35
+
36
+ ---
37
+
8
38
  ## [6.9.0] — 2026-10-01 — Reusable Codecs, Dynamic Pattern Derivation & Unified SDK
9
39
 
10
40
  ### Codecs & Type System
package/CLAUDE.md CHANGED
@@ -61,4 +61,4 @@ npm test # Run comprehensive test suite (unit tests, derivation tests, shell te
61
61
  - No external runtime dependencies (only jiwson for parser generation)
62
62
  - Uses ES6 modules (.mjs files) throughout
63
63
  - Extensive test suite in `tests/code-derivation/` for type system validation
64
- - REPL supports commands like `:C name` (show canonical type), `:t name` (show relation type), and `:d name` (show relation definition)
64
+ - REPL supports commands like `:rel name` (show relation definition), `:type name` (show type definition), `:types`, `:rels`, etc.
@@ -252,13 +252,11 @@ car = list? /cons .car;
252
252
  cdr = list? /cons .cdr;
253
253
  ```
254
254
 
255
- For example, the filter annotation for `car` is:
255
+ For example, the definition for `car` is:
256
256
 
257
257
  ```text
258
- > :t car
259
- car : ?<{X1 car, X0 cdr} cons, $KL nil>=X0 --> ?X1 (@...)
260
- > :C KL
261
- $ KL = {}; -- @...
258
+ > :rel car
259
+ car = list? /cons .car; -- @...
262
260
  ```
263
261
 
264
262
  ## Universal Schema Registry
package/DOCS/REPL.md CHANGED
@@ -33,16 +33,14 @@ complete.
33
33
  | Command | Meaning |
34
34
  | --- | --- |
35
35
  | `:help` | Show command summary |
36
+ | `:engine [wasm\|js]` | Display or switch evaluation engine (`wasm` or `js`) |
37
+ | `:wasm` / `:js` | Shortcut to switch engine |
38
+ | `:rel name` | Show relation definition |
36
39
  | `:type name` | Show the canonical definition of a type |
40
+ | `:types` | List type aliases |
41
+ | `:rels` | List relation aliases |
37
42
  | `:code name` | Alias for `:C name` |
38
- | `:run expr` | Evaluate `expr` on the current value |
39
- | `:eval expr` | Alias for `:run` |
40
- | `:time expr` | Evaluate `expr` and report compilation and execution time |
41
- | `:t name` | Show relation input/output filters |
42
- | `:d name` | Show relation definition |
43
43
  | `:C name` | Show canonical code definition |
44
- | `:codes` | List type aliases |
45
- | `:rels` | List relation aliases |
46
44
  | `:codec load file` | Load a codec from an ES module file (e.g. `:codec load codecs/int.mjs`) |
47
45
  | `:codec unload name` | Unload a registered codec |
48
46
  | `:codec list` | List loaded REPL codecs (alias: `:codecs`) |
@@ -208,21 +206,39 @@ Parses external input using the specified codec file.
208
206
 
209
207
  Entering `:input` with no arguments displays usage and a list of loaded codecs.
210
208
 
209
+ ## Evaluation Engines: WASM and JS
210
+
211
+ The REPL supports two evaluation engines:
212
+
213
+ - **`wasm`** (default): Compiles k expressions into WebAssembly bytecode via polymorphic kVM lowering. Provides near-native execution speed, tail-call optimization (TCO), and stack safety on deep recursion.
214
+ - **`js`**: The tree-walking JavaScript interpreter. Provides near-zero compilation latency (interprets parsed definitions directly) and convenient runtime inspection.
215
+
216
+ ### Engine Commands
217
+
218
+ - `:engine` &mdash; displays active engine (`wasm` or `js`)
219
+ - `:engine wasm` (or `:wasm`) &mdash; switches to the WebAssembly engine
220
+ - `:engine js` (or `:js`) &mdash; switches to the JavaScript engine
221
+
222
+ ### CLI Options
223
+
224
+ When starting `k-repl`, choose the engine with:
225
+
226
+ ```bash
227
+ k-repl --engine=js # or k-repl --js
228
+ k-repl --engine=wasm # or k-repl --wasm (default)
229
+ ```
230
+
211
231
  ## Timing and Profiling
212
232
 
213
233
  Timing reporting is enabled by default for all evaluations, reporting elapsed time
214
- broken down into compilation (type derivation + WebAssembly lowering/compilation) and runtime execution:
234
+ broken down into compilation and runtime execution, annotated with the active engine:
215
235
 
216
236
  ```text
217
237
  > {10 int x, 5 int y} plus
218
238
  {}|_|1|1|1|1|+ ?<{} _, ...>
219
- /* comp: 12.4ms, exec: 0.8ms */
239
+ /* comp: 12.4ms, exec: 0.8ms (wasm) */
220
240
  ```
221
241
 
222
- ### `:time expr`
223
-
224
- Evaluates `expr` with explicit timing and records performance metrics in session state.
225
-
226
242
  ## Web REPL (Studio)
227
243
 
228
244
  The repository provides a standalone, single-file browser REPL: `repl.html`.
@@ -274,8 +290,8 @@ Evaluated values print in k syntax together with the inferred envelope:
274
290
  > inc = | +1;
275
291
  > {} | 0
276
292
  {}|0 ?<{} 0, ...>
277
- > :t inc
278
- inc : ?X0 --> ?<X0 +1, ...> (@...)
293
+ > :rel inc
294
+ inc = | +1; -- @...
279
295
  > :klib nat.klib
280
296
  saved nat.klib
281
297
  > :ko inc.ko inc
@@ -563,6 +563,7 @@ k_value *k_rt_compact(k_rt *rt, k_value *root, k_rt_checkpoint *mark_ptr) {
563
563
 
564
564
  if (!is_after_mark(rt, root, *mark_ptr)) {
565
565
  rt_rewind_and_free(rt, *mark_ptr);
566
+ *mark_ptr = k_rt_mark(rt);
566
567
  return root;
567
568
  }
568
569
 
@@ -580,6 +581,7 @@ k_value *k_rt_compact(k_rt *rt, k_value *root, k_rt_checkpoint *mark_ptr) {
580
581
  if (staging_used == 0) {
581
582
  free(staging);
582
583
  if (fixups.offsets != NULL) free(fixups.offsets);
584
+ *mark_ptr = k_rt_mark(rt);
583
585
  return staged_root;
584
586
  }
585
587
 
@@ -603,6 +605,8 @@ k_value *k_rt_compact(k_rt *rt, k_value *root, k_rt_checkpoint *mark_ptr) {
603
605
  free(staging);
604
606
  if (fixups.offsets != NULL) free(fixups.offsets);
605
607
 
608
+ *mark_ptr = k_rt_mark(rt);
609
+
606
610
  return new_root;
607
611
  }
608
612
 
@@ -438,7 +438,7 @@ function hasSelfTailCall(insts, funcName, isTail = true) {
438
438
  for (let i = 0; i < insts.length; i++) {
439
439
  const inst = insts[i];
440
440
  if (isTail && isSelfTailCall(insts, i, funcName)) return true;
441
- if (inst.branches) {
441
+ if (inst.op === "union" && inst.branches) {
442
442
  const unionIsTail = isTail && tailValueAfter(insts, i + 1, inst.dest);
443
443
  for (let bIdx = 0; bIdx < inst.branches.length; bIdx++) {
444
444
  if (hasSelfTailCall(inst.branches[bIdx].body, funcName, unionIsTail)) return true;
@@ -721,7 +721,7 @@ function lowerKVMFunction(kvmFunc, symbol, funcName, moduleCtx, linkage = "", op
721
721
  }
722
722
  case "call": {
723
723
  const v = getReg(inst.src);
724
- if (isSelfTailCall(insts, i, tailRef)) {
724
+ if (isTopLevel && !returnTarget && isSelfTailCall(insts, i, tailRef)) {
725
725
  if (ctx.catchTail) {
726
726
  const compacted = lowerRtCompact(ctx, v, "%tail_mark_slot");
727
727
  ctx.lines.push(` store ptr ${compacted}, ptr %tail_input_slot`);
@@ -237,4 +237,23 @@ assert.match(nestedUnionLLVM, /store ptr %union_value\d+, ptr %ptr_slot\d+/);
237
237
  assert.doesNotMatch(nestedUnionLLVM, /call void @k_product_set_at\(ptr %product\d+/);
238
238
  assert.doesNotMatch(nestedUnionLLVM, /ptr false/);
239
239
 
240
+ const productRecurseObject = decodeObject(compileObjectBuffer(`
241
+ $ list = < {} nil, { {} car, list cdr } cons >;
242
+ foo = $list <
243
+ /nil |nil,
244
+ /cons { .car car, .cdr foo cdr } |cons
245
+ > $list;
246
+ foo
247
+ `, { source: "llvm-product-recurse.k" }));
248
+ const { llvm: productRecurseLLVM } = compileObjectToLLVM(productRecurseObject, {
249
+ relation: "foo",
250
+ inputPattern: [
251
+ ["closed-union", [["nil", 1], ["cons", 2]]],
252
+ ["closed-product", []],
253
+ ["closed-product", [["car", 1], ["cdr", 0]]]
254
+ ]
255
+ });
256
+ // Calls inside product fields must not return tail status (i32 2)
257
+ assert.doesNotMatch(productRecurseLLVM, /insertvalue %k_result undef, i32 2, 0/);
258
+
240
259
  console.log("OK");
@@ -397,6 +397,8 @@ export function lowerToWasm(relDef, name, options = {}) {
397
397
  }
398
398
  lines.push(` local.get $tail_loop_mark`);
399
399
  lines.push(` call $compact_finish`);
400
+ lines.push(` global.get $arena_free`);
401
+ lines.push(` local.set $tail_loop_mark`);
400
402
  lines.push(` end`);
401
403
  for (let i = 0; i < N; i++) {
402
404
  const branch = sortedBranches[i];
@@ -598,6 +600,8 @@ export function lowerToWasm(relDef, name, options = {}) {
598
600
  lines.push(` local.set $${callSrc}`);
599
601
  lines.push(` local.get $tail_loop_mark`);
600
602
  lines.push(` call $compact_finish`);
603
+ lines.push(` global.get $arena_free`);
604
+ lines.push(` local.set $tail_loop_mark`);
601
605
  lines.push(` end`);
602
606
  lines.push(` local.get $${callSrc}`);
603
607
  lines.push(` local.set $in`);
@@ -619,6 +619,10 @@ console.log("==> Running End-to-End Peano Addition & Serialization Tests");
619
619
  const deepRes = exports.add(deepInputPtr);
620
620
  assert.equal(deepRes[1], 1, "Tail-recursive Peano addition must handle deep inputs without overflowing the Wasm call stack");
621
621
 
622
+ const deeperInputPtr = writeAddInputPtrToArena(writeNatPtrToArena(20000), writeNatPtrToArena(0));
623
+ const deeperRes = exports.add(deeperInputPtr);
624
+ assert.equal(deeperRes[1], 1, "Tail-recursive Peano addition must handle 20000 inputs with compaction without call stack overflow");
625
+
622
626
  console.log("End-to-End Peano Addition & Serialization tests passed successfully!");
623
627
  }
624
628
 
@@ -302,11 +302,24 @@ export async function executeCommand(input) {
302
302
  // Re-create completer with updated state
303
303
  completer = createCompleter(state);
304
304
  updateCodecsBadge();
305
+ updateEngineUI();
305
306
  if (codecsModal && codecsModal.classList.contains("open")) {
306
307
  renderCodecsModal();
307
308
  }
308
309
  }
309
310
 
311
+ function updateEngineUI() {
312
+ const badge = document.getElementById("engine-badge");
313
+ if (!badge) return;
314
+ badge.textContent = state.engine === "js" ? "js-in-process" : "wasm-in-process";
315
+ badge.title = `Engine: ${state.engine} (click to toggle wasm/js)`;
316
+ if (state.engine === "js") {
317
+ badge.classList.add("js-mode");
318
+ } else {
319
+ badge.classList.remove("js-mode");
320
+ }
321
+ }
322
+
310
323
  function adjustInputHeight() {
311
324
  if (!inputEl) return;
312
325
  inputEl.style.height = "auto";
@@ -949,6 +962,16 @@ export function initRepl() {
949
962
  }
950
963
  });
951
964
 
965
+ // Engine Badge toggle (wasm <-> js)
966
+ const engineBadge = document.getElementById("engine-badge");
967
+ if (engineBadge) {
968
+ engineBadge.onclick = () => {
969
+ const nextEngine = state.engine === "wasm" ? "js" : "wasm";
970
+ executeCommand(`:engine ${nextEngine}`);
971
+ };
972
+ }
973
+ updateEngineUI();
974
+
952
975
  inputEl.focus();
953
976
  }
954
977
 
@@ -965,6 +988,8 @@ if (typeof window !== "undefined") {
965
988
  executeCommand,
966
989
  initRepl,
967
990
  getState: () => state,
991
+ getEngine: () => state.engine,
992
+ setEngine: (engine) => executeCommand(`:engine ${engine}`),
968
993
  getVfsFile,
969
994
  setVfsFile,
970
995
  getAllVfsFiles,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fraczak/k",
3
- "version": "6.9.0",
3
+ "version": "6.10.1",
4
4
  "description": "k — a language for composing first-order partial functions over algebraic data types (products and tagged unions); includes runtime, type derivation, REPL, and codec toolchain",
5
5
  "main": "index.mjs",
6
6
  "exports": {
package/repl.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { argv, exit, stdin, stdout } from "node:process";
9
9
 
10
10
  import { annotate, parse } from "./index.mjs";
11
- import { constrainWithPattern } from "./run.mjs";
11
+ import run, { constrainWithPattern, run_rel } from "./run.mjs";
12
12
  import { compileWasmArtifactFromObject, instantiateWasmArtifact } from "./backends/wasm/src/wasm.mjs";
13
13
  import { exportPatternGraph } from "./codecs/runtime/codec.mjs";
14
14
  import { patternToPropertyList } from "./codecs/runtime/pattern-json.mjs";
@@ -21,7 +21,7 @@ if (typeof globalThis !== "undefined") {
21
21
  globalThis.isVariant = isVariant;
22
22
  globalThis.patternFromFilter = patternFromFilter;
23
23
  }
24
- import { patterns2filters, prettyCode, prettyRel } from "./pretty.mjs";
24
+ import { prettyCode } from "./pretty.mjs";
25
25
  import {
26
26
  compileObject,
27
27
  compileLibrary,
@@ -48,9 +48,9 @@ import {
48
48
 
49
49
  const NAME_RE = /^[a-zA-Z0-9_+-][a-zA-Z0-9_?!+-]*$/;
50
50
  const COMMAND_NAMES = [
51
- "help", "type", "code", "run", "eval", "t", "d", "C",
52
- "codes", "codecs", "rels", "codec", "input", "reset", "klib", "ko", "load",
53
- "time",
51
+ "help", "type", "code", "rel", "C",
52
+ "types", "codecs", "rels", "codec", "input", "reset", "klib", "ko", "load",
53
+ "engine", "wasm", "js",
54
54
  "quit", "exit"
55
55
  ];
56
56
  const CODEC_COMMAND_NAMES = ["load", "unload", "list"];
@@ -73,9 +73,14 @@ function emptyValue() {
73
73
  return Value.product({}, [["closed-product", []]]);
74
74
  }
75
75
 
76
- function createState() {
76
+ function createState(options = {}) {
77
77
  codes.load(cloneJSON(initialCodes));
78
+ const engine = (options.engine || "wasm").toLowerCase();
79
+ if (engine !== "wasm" && engine !== "js") {
80
+ throw new Error(`Invalid engine '${options.engine}'. Valid engines: wasm, js`);
81
+ }
78
82
  return {
83
+ engine,
79
84
  codes: codes.dump(),
80
85
  rels: {},
81
86
  relAliases: {},
@@ -337,11 +342,6 @@ function resolveRel(state, name) {
337
342
  return hash ? { hash, rel: state.rels[hash] } : { hash: null, rel: null };
338
343
  }
339
344
 
340
- function relTypeString(rel) {
341
- const filters = patterns2filters(rel.typePatternGraph, ...rel.def.patterns);
342
- return filters.map((filter) => prettyRel({ op: "filter", filter })).join(" --> ");
343
- }
344
-
345
345
  function listAliases(aliases) {
346
346
  const entries = Object.entries(aliases).sort(([a], [b]) => a.localeCompare(b));
347
347
  if (entries.length === 0) return "(none)";
@@ -803,6 +803,10 @@ function completeCommandArgument(line, state) {
803
803
  return completeCodecCommand(line, argStart, arg, state);
804
804
  }
805
805
 
806
+ if (command === "engine") {
807
+ return completeCommandWord(line, argStart, arg, ["wasm", "js"]);
808
+ }
809
+
806
810
  if (command === "input") {
807
811
  return completeInputCommand(line, state, argStart, arg);
808
812
  }
@@ -1047,47 +1051,24 @@ async function executeExpressionWithWasm(annotated, state, lineOffset = 0) {
1047
1051
  return { result, wasmCompileMs, executeMs };
1048
1052
  }
1049
1053
 
1050
- async function runExpression(input, state) {
1051
- const expression = input.trim();
1052
- if (!expression) throw new Error(":run requires an expression");
1053
-
1054
- restoreCodes(state);
1055
- const preamble = aliasPreamble(state);
1056
- const lineOffset = preambleLineCount(preamble);
1057
- const source = [preamble, expression].filter(Boolean).join("\n");
1058
- const compileStart = performance.now();
1059
- let annotated;
1060
- try {
1061
- annotated = annotate(source, { libraries: [stateLibrary(state)] });
1062
- } catch (error) {
1063
- throw remapError(error, lineOffset);
1064
- }
1065
- const snippetCompileMs = performance.now() - compileStart;
1054
+ function executeExpressionWithJs(annotated, state, lineOffset = 0) {
1055
+ const mainRel = annotated.rels.__main__;
1056
+ if (!mainRel) return { result: undefined, wasmCompileMs: 0, executeMs: 0 };
1066
1057
 
1067
- let execRes;
1068
- try {
1069
- execRes = await executeExpressionWithWasm(annotated, state, lineOffset);
1070
- } catch (error) {
1071
- throw remapError(error, lineOffset);
1058
+ let inputValue = state.value ?? emptyValue();
1059
+ if (mainRel.typePatternGraph && mainRel.def.patterns) {
1060
+ const graph = mainRel.typePatternGraph;
1061
+ const nodeId = graph.find(mainRel.def.patterns[0]);
1062
+ const inputPattern = patternToPropertyList(exportPatternGraph(graph, nodeId));
1063
+ inputValue = constrainWithPattern(inputValue, inputPattern, mainRel.def);
1072
1064
  }
1073
- state.codes = codes.dump();
1074
- restoreCodes(state);
1075
1065
 
1076
- const compileMs = snippetCompileMs + execRes.wasmCompileMs;
1077
- const executeMs = execRes.executeMs;
1078
- state.lastTiming = {
1079
- compileMs,
1080
- executeMs,
1081
- totalMs: compileMs + executeMs,
1082
- snippetCompileMs,
1083
- wasmCompileMs: execRes.wasmCompileMs
1084
- };
1066
+ const executeStart = performance.now();
1067
+ run.defs = { rels: annotated.rels };
1068
+ const result = run_rel(codes.find, mainRel, inputValue, "__main__");
1069
+ const executeMs = performance.now() - executeStart;
1085
1070
 
1086
- const committed = commitResult(state, execRes.result, expression);
1087
- if (state.showTiming) {
1088
- committed.push(`/* comp: ${formatDuration(compileMs)}, exec: ${formatDuration(executeMs)} */`);
1089
- }
1090
- return committed;
1071
+ return { result, wasmCompileMs: 0, executeMs };
1091
1072
  }
1092
1073
 
1093
1074
  async function runSnippet(input, state, options = {}) {
@@ -1108,7 +1089,8 @@ async function runSnippet(input, state, options = {}) {
1108
1089
  executeMs: 0,
1109
1090
  totalMs: snippetCompileMs,
1110
1091
  snippetCompileMs,
1111
- wasmCompileMs: 0
1092
+ wasmCompileMs: 0,
1093
+ engine: state.engine
1112
1094
  };
1113
1095
  if (state.showTiming) {
1114
1096
  return [`/* comp: ${formatDuration(snippetCompileMs)} */`];
@@ -1118,26 +1100,32 @@ async function runSnippet(input, state, options = {}) {
1118
1100
 
1119
1101
  let execRes;
1120
1102
  try {
1121
- execRes = await executeExpressionWithWasm(annotated, state, lineOffset);
1103
+ if (state.engine === "js") {
1104
+ execRes = executeExpressionWithJs(annotated, state, lineOffset);
1105
+ } else {
1106
+ execRes = await executeExpressionWithWasm(annotated, state, lineOffset);
1107
+ }
1122
1108
  } catch (error) {
1123
1109
  throw remapError(error, lineOffset);
1124
1110
  }
1125
1111
  state.codes = codes.dump();
1126
1112
  restoreCodes(state);
1127
1113
 
1128
- const compileMs = snippetCompileMs + execRes.wasmCompileMs;
1114
+ const wasmCompileMs = execRes.wasmCompileMs || 0;
1115
+ const compileMs = snippetCompileMs + wasmCompileMs;
1129
1116
  const executeMs = execRes.executeMs;
1130
1117
  state.lastTiming = {
1131
1118
  compileMs,
1132
1119
  executeMs,
1133
1120
  totalMs: compileMs + executeMs,
1134
1121
  snippetCompileMs,
1135
- wasmCompileMs: execRes.wasmCompileMs
1122
+ wasmCompileMs,
1123
+ engine: state.engine
1136
1124
  };
1137
1125
 
1138
1126
  const committed = commitResult(state, execRes.result, snippet);
1139
1127
  if (state.showTiming) {
1140
- committed.push(`/* comp: ${formatDuration(compileMs)}, exec: ${formatDuration(executeMs)} */`);
1128
+ committed.push(`/* comp: ${formatDuration(compileMs)}, exec: ${formatDuration(executeMs)} (${state.engine}) */`);
1141
1129
  }
1142
1130
  return committed;
1143
1131
  }
@@ -1267,10 +1255,6 @@ async function evaluateCommand(line, state) {
1267
1255
  return [helpText()];
1268
1256
  case "timing":
1269
1257
  throw new Error(":timing has been removed; timing is now always enabled");
1270
- case "time": {
1271
- if (!arg) throw new Error(":time requires an expression");
1272
- return evaluateInput(arg, state);
1273
- }
1274
1258
  case "type": {
1275
1259
  if (!arg) throw new Error(":type requires a type name");
1276
1260
  if (arg.includes("=")) {
@@ -1280,13 +1264,18 @@ async function evaluateCommand(line, state) {
1280
1264
  }
1281
1265
  case "code":
1282
1266
  return evaluateCommand(`:C ${arg}`, state);
1283
- case "rel":
1284
1267
  case "def":
1285
1268
  throw new Error("Relation definitions use syntax: name = relExpr;");
1286
- case "run":
1287
- case "eval":
1288
- return runExpression(arg, state);
1289
- case "codes":
1269
+ case "rel": {
1270
+ if (!arg) throw new Error(`${usagePrefix}rel requires a relation name`);
1271
+ if (arg.includes("=")) {
1272
+ throw new Error("Relation definitions use syntax: name = relExpr; Use :rel <name> to show a relation definition");
1273
+ }
1274
+ const { hash, rel } = resolveRel(state, arg);
1275
+ if (!rel) throw new Error(`Unknown relation '${arg}'`);
1276
+ return [`${arg} = ${prettyRelation(rel, displayAliases(state), state.relAliases)}; -- ${hash}`];
1277
+ }
1278
+ case "types":
1290
1279
  return [listAliases(state.typeAliases)];
1291
1280
  case "rels":
1292
1281
  return [listAliases(state.relAliases)];
@@ -1295,8 +1284,28 @@ async function evaluateCommand(line, state) {
1295
1284
  return loadCodec(arg, state);
1296
1285
  case "input":
1297
1286
  return requestCodecInput(arg, state);
1287
+ case "engine": {
1288
+ const mode = (arg || "").trim().toLowerCase();
1289
+ if (!mode) {
1290
+ return [`Current engine: ${state.engine}`];
1291
+ }
1292
+ if (mode !== "wasm" && mode !== "js") {
1293
+ throw new Error(`Invalid engine '${arg.trim()}'. Valid engines: wasm, js`);
1294
+ }
1295
+ state.engine = mode;
1296
+ return [`engine set to ${mode}`];
1297
+ }
1298
+ case "wasm": {
1299
+ state.engine = "wasm";
1300
+ return ["engine set to wasm"];
1301
+ }
1302
+ case "js": {
1303
+ state.engine = "js";
1304
+ return ["engine set to js"];
1305
+ }
1298
1306
  case "reset": {
1299
- const fresh = createState();
1307
+ const currentEngine = state.engine;
1308
+ const fresh = createState({ engine: currentEngine });
1300
1309
  Object.assign(state, fresh);
1301
1310
  return ["reset"];
1302
1311
  }
@@ -1321,18 +1330,6 @@ async function evaluateCommand(line, state) {
1321
1330
  loadSourceOrKlib(state, loadPath, { loadAliases });
1322
1331
  return [`loaded ${loadPath}`];
1323
1332
  }
1324
- case "t": {
1325
- if (!arg) throw new Error(`${usagePrefix}t requires a relation name`);
1326
- const { hash, rel } = resolveRel(state, arg);
1327
- if (!rel) throw new Error(`Unknown relation '${arg}'`);
1328
- return [`${arg} : ${relTypeString(rel)} (${hash})`];
1329
- }
1330
- case "d": {
1331
- if (!arg) throw new Error(`${usagePrefix}d requires a relation name`);
1332
- const { hash, rel } = resolveRel(state, arg);
1333
- if (!rel) throw new Error(`Unknown relation '${arg}'`);
1334
- return [`${arg} = ${prettyRelation(rel, displayAliases(state), state.relAliases)}; -- ${hash}`];
1335
- }
1336
1333
  case "C": {
1337
1334
  if (!arg) throw new Error(`${usagePrefix}C requires a type name`);
1338
1335
  const hash = state.typeAliases[arg] || (arg.startsWith("@") ? arg : null);
@@ -1346,12 +1343,10 @@ async function evaluateCommand(line, state) {
1346
1343
 
1347
1344
  function helpText() {
1348
1345
  return [
1349
- ":run expr run an expression on the current value",
1350
- ":time expr run an expression and report compilation/execution time",
1351
- ":t name show relation type",
1352
- ":d name show relation definition",
1346
+ ":engine [wasm|js] display or switch evaluation engine (wasm or js)",
1347
+ ":rel name show relation definition",
1353
1348
  ":type name show type definition",
1354
- ":codes list type aliases",
1349
+ ":types list type aliases",
1355
1350
  ":rels list relation aliases",
1356
1351
  ":codec load file load a codec module from file (e.g. :codec load codecs/int.mjs)",
1357
1352
  ":codec unload name unload a registered codec",
@@ -1376,17 +1371,19 @@ function promptForState(state) {
1376
1371
  }
1377
1372
 
1378
1373
  function cliUsage() {
1379
- console.log("Usage: k-repl");
1380
- console.log(" k-repl -h");
1374
+ console.log("Usage: k-repl [options]");
1381
1375
  console.log("");
1382
1376
  console.log("Start the interactive k interpreter. Type :help inside the REPL for commands.");
1383
1377
  console.log("");
1384
1378
  console.log("Options:");
1385
- console.log(" -h, --help Show this help.");
1379
+ console.log(" --engine=<wasm|js> Select evaluation engine (default: wasm)");
1380
+ console.log(" --js Shortcut for --engine=js");
1381
+ console.log(" --wasm Shortcut for --engine=wasm");
1382
+ console.log(" -h, --help Show this help.");
1386
1383
  }
1387
1384
 
1388
- function startRepl() {
1389
- const state = createState();
1385
+ function startRepl(options = {}) {
1386
+ const state = createState(options);
1390
1387
  const rl = readline.createInterface({
1391
1388
  input: stdin,
1392
1389
  output: stdout,
@@ -1397,7 +1394,7 @@ function startRepl() {
1397
1394
  let pending = Promise.resolve();
1398
1395
  let closed = false;
1399
1396
 
1400
- console.log("k interpreter (.klib-backed). Type :help for commands.");
1397
+ console.log(`k interpreter (.klib-backed, ${state.engine}). Type :help for commands.`);
1401
1398
  rl.prompt();
1402
1399
  async function handleLine(line) {
1403
1400
  if (state.pendingInput && buffer.length === 0) {
@@ -1476,15 +1473,38 @@ function startRepl() {
1476
1473
 
1477
1474
  if (isMainEntrypoint()) {
1478
1475
  const args = argv.slice(2);
1479
- if (args.includes("-h") || args.includes("--help")) {
1480
- cliUsage();
1481
- exit(0);
1482
- }
1483
- if (args.length > 0) {
1484
- cliUsage();
1485
- exit(1);
1476
+ let engine = "wasm";
1477
+ for (let i = 0; i < args.length; i++) {
1478
+ const arg = args[i];
1479
+ if (arg === "-h" || arg === "--help") {
1480
+ cliUsage();
1481
+ exit(0);
1482
+ } else if (arg === "--js") {
1483
+ engine = "js";
1484
+ } else if (arg === "--wasm") {
1485
+ engine = "wasm";
1486
+ } else if (arg.startsWith("--engine=")) {
1487
+ const val = arg.slice("--engine=".length).toLowerCase();
1488
+ if (val !== "wasm" && val !== "js") {
1489
+ console.error(`Invalid engine '${val}'. Valid engines: wasm, js`);
1490
+ exit(1);
1491
+ }
1492
+ engine = val;
1493
+ } else if (arg === "--engine") {
1494
+ i++;
1495
+ const val = (args[i] || "").toLowerCase();
1496
+ if (val !== "wasm" && val !== "js") {
1497
+ console.error(`Invalid engine '${val}'. Valid engines: wasm, js`);
1498
+ exit(1);
1499
+ }
1500
+ engine = val;
1501
+ } else {
1502
+ console.error(`Unknown option '${arg}'.`);
1503
+ cliUsage();
1504
+ exit(1);
1505
+ }
1486
1506
  }
1487
- startRepl();
1507
+ startRepl({ engine });
1488
1508
  }
1489
1509
 
1490
1510
  export {
@@ -247,6 +247,25 @@ const htmlContent = `<!DOCTYPE html>
247
247
  border: 1px solid rgba(88, 166, 255, 0.3);
248
248
  padding: 2px 8px;
249
249
  border-radius: 12px;
250
+ cursor: pointer;
251
+ user-select: none;
252
+ transition: all 0.15s ease;
253
+ }
254
+
255
+ .engine-badge:hover {
256
+ background: rgba(88, 166, 255, 0.3);
257
+ border-color: rgba(88, 166, 255, 0.6);
258
+ }
259
+
260
+ .engine-badge.js-mode {
261
+ background: rgba(240, 180, 41, 0.15);
262
+ color: #e3b341;
263
+ border-color: rgba(240, 180, 41, 0.4);
264
+ }
265
+
266
+ .engine-badge.js-mode:hover {
267
+ background: rgba(240, 180, 41, 0.3);
268
+ border-color: rgba(240, 180, 41, 0.7);
250
269
  }
251
270
 
252
271
  .status-indicator {
@@ -1609,7 +1628,7 @@ const htmlContent = `<!DOCTYPE html>
1609
1628
  <div class="nav-left">
1610
1629
  <div class="brand">
1611
1630
  <span class="brand-name">k repl</span>
1612
- <span class="engine-badge">wasm-in-process</span>
1631
+ <span id="engine-badge" class="engine-badge" title="Click to toggle engine (wasm/js)">wasm-in-process</span>
1613
1632
  </div>
1614
1633
  <div class="status-indicator">
1615
1634
  <span id="status-dot" class="status-dot ready"></span>
@@ -1635,8 +1654,11 @@ const htmlContent = `<!DOCTYPE html>
1635
1654
  <option value="expr:{10 x, 5 y} plus">{10 x, 5 y} plus (bits)</option>
1636
1655
  <option value="expr:1 succ">1 succ</option>
1637
1656
  <option value="expr:{10 int x, 5 int y} plus">{10 int x, 5 int y} plus (int)</option>
1638
- <option value="expr::codes">:codes (list types)</option>
1657
+ <option value="expr::types">:types (list types)</option>
1639
1658
  <option value="expr::rels">:rels (list relations)</option>
1659
+ <option value="expr::engine">:engine (show engine)</option>
1660
+ <option value="expr::engine wasm">:engine wasm</option>
1661
+ <option value="expr::engine js">:engine js</option>
1640
1662
  </optgroup>
1641
1663
  <optgroup label="Codecs &amp; Format Adapters">
1642
1664
  <option value="expr::codecs">:codecs</option>
@@ -1856,17 +1878,16 @@ const htmlContent = `<!DOCTYPE html>
1856
1878
  <div class="help-section">
1857
1879
  <h4>REPL Commands</h4>
1858
1880
  <table class="help-table">
1881
+ <tr><td><code>:engine [wasm|js]</code></td><td>Display or switch evaluation engine (<code>wasm</code> or <code>js</code>)</td></tr>
1859
1882
  <tr><td><code>:load &lt;file&gt;</code></td><td>Load a <code>.k</code> source or <code>.klib</code> library into current state</td></tr>
1860
- <tr><td><code>:t &lt;name&gt;</code></td><td>Display the derived type of relation <code>&lt;name&gt;</code></td></tr>
1861
- <tr><td><code>:d &lt;name&gt;</code></td><td>Display definition of relation <code>&lt;name&gt;</code></td></tr>
1883
+ <tr><td><code>:rel &lt;name&gt;</code></td><td>Display definition of relation <code>&lt;name&gt;</code></td></tr>
1862
1884
  <tr><td><code>:type &lt;name&gt;</code></td><td>Display definition of type <code>&lt;name&gt;</code></td></tr>
1863
- <tr><td><code>:codes</code></td><td>List all type aliases and canonical hashes</td></tr>
1885
+ <tr><td><code>:types</code></td><td>List all type aliases and canonical hashes</td></tr>
1864
1886
  <tr><td><code>:rels</code></td><td>List all relation aliases</td></tr>
1865
1887
  <tr><td><code>:codecs</code></td><td>List all loaded format codecs</td></tr>
1866
1888
  <tr><td><code>:codec load &lt;file.mjs&gt;</code></td><td>Load a codec from file (or load through Files)</td></tr>
1867
1889
  <tr><td><code>:codec unload &lt;file.mjs&gt;</code></td><td>Unload a loaded codec</td></tr>
1868
1890
  <tr><td><code>:input &lt;codec.mjs&gt; [text]</code></td><td>Parse input using specified codec (opens dialog if text omitted)</td></tr>
1869
- <tr><td><code>:time &lt;expr&gt;</code></td><td>Evaluate <code>&lt;expr&gt;</code> and report execution time</td></tr>
1870
1891
  <tr><td><code>:klib &lt;file&gt;</code></td><td>Export current state as a <code>.klib</code> binary file</td></tr>
1871
1892
  <tr><td><code>:reset</code></td><td>Clear state and reset to initial environment</td></tr>
1872
1893
  <tr><td><code>:help</code></td><td>Show command reference</td></tr>
@@ -539,11 +539,17 @@ if (chromiumBin) {
539
539
  btnHelp.click();
540
540
  const helpModalOpen = document.getElementById("help-modal")?.classList.contains("open");
541
541
  const modalText = document.getElementById("help-modal")?.textContent || "";
542
- const hasRel = modalText.includes(":rel ");
542
+ const hasRel = modalText.includes(":rel <name>");
543
543
  const hasVal = modalText.includes(":val");
544
544
  const hasTiming = modalText.includes(":timing");
545
+ const hasTime = modalText.includes(":time");
546
+ const hasT = modalText.includes(":t <name>");
545
547
  const hasCodecDefine = modalText.includes(":codec define");
546
548
  const hasType = modalText.includes(":type <name>");
549
+ const hasTypes = modalText.includes(":types");
550
+ const hasCodes = modalText.includes(":codes");
551
+ const hasD = modalText.includes(":d <name>");
552
+ const hasEngine = modalText.includes(":engine");
547
553
  return {
548
554
  found: true,
549
555
  isVisible,
@@ -552,18 +558,48 @@ if (chromiumBin) {
552
558
  hasRel,
553
559
  hasVal,
554
560
  hasTiming,
561
+ hasTime,
562
+ hasT,
555
563
  hasCodecDefine,
556
- hasType
564
+ hasType,
565
+ hasTypes,
566
+ hasCodes,
567
+ hasD,
568
+ hasEngine
557
569
  };
558
570
  })()`);
559
571
  assert.strictEqual(narrowNavbarCheck.found, true, "#btn-help should exist");
560
572
  assert.strictEqual(narrowNavbarCheck.isVisible, true, "#btn-help should be visible within narrow viewport");
561
573
  assert.strictEqual(narrowNavbarCheck.helpModalOpen, true, "Clicking #btn-help in narrow viewport should open help modal");
562
- assert.strictEqual(narrowNavbarCheck.hasRel, false, "Help modal should not contain :rel");
574
+ assert.strictEqual(narrowNavbarCheck.hasRel, true, "Help modal should contain :rel <name>");
575
+ assert.strictEqual(narrowNavbarCheck.hasType, true, "Help modal should contain :type <name>");
576
+ assert.strictEqual(narrowNavbarCheck.hasTypes, true, "Help modal should contain :types");
577
+ assert.strictEqual(narrowNavbarCheck.hasEngine, true, "Help modal should contain :engine");
578
+ assert.strictEqual(narrowNavbarCheck.hasCodes, false, "Help modal should not contain :codes");
579
+ assert.strictEqual(narrowNavbarCheck.hasTime, false, "Help modal should not contain :time");
580
+ assert.strictEqual(narrowNavbarCheck.hasT, false, "Help modal should not contain :t <name>");
581
+ assert.strictEqual(narrowNavbarCheck.hasD, false, "Help modal should not contain :d <name>");
563
582
  assert.strictEqual(narrowNavbarCheck.hasVal, false, "Help modal should not contain :val");
564
583
  assert.strictEqual(narrowNavbarCheck.hasTiming, false, "Help modal should not contain :timing");
565
584
  assert.strictEqual(narrowNavbarCheck.hasCodecDefine, false, "Help modal should not contain :codec define");
566
- assert.strictEqual(narrowNavbarCheck.hasType, true, "Help modal should contain :type <name>");
585
+
586
+ // 6. Test dual engine switching in browser (wasm <-> js)
587
+ const engineCheck = await evaluateAsync(`(async () => {
588
+ const initialEngine = window.kRepl.getEngine();
589
+ await window.kRepl.executeCommand(":engine js");
590
+ const jsEngine = window.kRepl.getEngine();
591
+ const badgeJsText = document.getElementById("engine-badge")?.textContent;
592
+ await window.kRepl.executeCommand("() | ok");
593
+ await window.kRepl.executeCommand(":engine wasm");
594
+ const wasmEngine = window.kRepl.getEngine();
595
+ const badgeWasmText = document.getElementById("engine-badge")?.textContent;
596
+ return { initialEngine, jsEngine, wasmEngine, badgeJsText, badgeWasmText };
597
+ })()`);
598
+ assert.strictEqual(engineCheck.initialEngine, "wasm");
599
+ assert.strictEqual(engineCheck.jsEngine, "js");
600
+ assert.strictEqual(engineCheck.badgeJsText, "js-in-process");
601
+ assert.strictEqual(engineCheck.wasmEngine, "wasm");
602
+ assert.strictEqual(engineCheck.badgeWasmText, "wasm-in-process");
567
603
 
568
604
  ws.close();
569
605
  console.log(" Browser execution verified successfully!");
@@ -29,16 +29,16 @@ output = await evaluateInput(":type nat", state);
29
29
  assert.match(output[0], /^\$ nat = /);
30
30
 
31
31
  output = await evaluateInput("succ = | succ;", state);
32
- output = await evaluateInput(":d succ", state);
32
+ output = await evaluateInput(":rel succ", state);
33
33
  assert.match(output[0], /^succ = /);
34
34
 
35
- output = await evaluateInput(":codes", state);
35
+ output = await evaluateInput(":types", state);
36
36
  assert.match(output[0], /^nat = @/m);
37
37
 
38
38
  output = await evaluateInput(":rels", state);
39
39
  assert.match(output[0], /^succ = @/m);
40
40
 
41
- output = await evaluateInput(":run succ", state);
41
+ output = await evaluateInput("succ", state);
42
42
  assert.equal(output[0], "{}|succ ?<{} succ, ...>");
43
43
 
44
44
  output = await evaluateInput("/zero", state);
@@ -48,24 +48,38 @@ assert.equal(state.value.toJSON(), "succ");
48
48
  output = await evaluateInput("succ", state);
49
49
  assert.match(output[0], /^\{\}\|succ\|succ \?</);
50
50
 
51
- output = await evaluateInput(":t succ", state);
52
- assert.match(output[0], /^succ : /);
53
-
54
- output = await evaluateInput(":d succ", state);
51
+ output = await evaluateInput(":rel succ", state);
55
52
  assert.match(output[0], /^succ = /);
56
53
 
57
54
  let completions = completeInput(":he", state)[0];
58
55
  assert.deepEqual(completions, [":help"]);
59
56
 
60
57
  const canonicalPartial = state.typeAliases.nat.slice(0, 8);
61
- completions = completeInput(`:run ${canonicalPartial}`, state)[0];
58
+ completions = completeInput(`:rel ${canonicalPartial}`, state)[0];
62
59
  assert(completions.some((line) => line.endsWith(state.typeAliases.nat)));
63
60
 
64
61
  completions = completeInput(`(${canonicalPartial}`, state)[0];
65
62
  assert(completions.includes(`(${state.typeAliases.nat}`));
66
63
 
67
- completions = completeInput(":run su", state)[0];
68
- assert(completions.includes(":run succ"));
64
+ completions = completeInput(":rel su", state)[0];
65
+ assert(completions.includes(":rel succ"));
66
+ completions = completeInput(":ty", state)[0];
67
+ assert(completions.includes(":types"));
68
+ assert(completions.includes(":type"));
69
+
70
+ await assert.rejects(
71
+ () => evaluateInput(":run succ", state),
72
+ /Unknown command ':run'/
73
+ );
74
+ await assert.rejects(
75
+ () => evaluateInput(":time ()", state),
76
+ /Unknown command ':time'/
77
+ );
78
+ await assert.rejects(
79
+ () => evaluateInput(":t succ", state),
80
+ /Unknown command ':t'/
81
+ );
82
+
69
83
  completions = completeInput(":co", state)[0];
70
84
  assert(completions.includes(":codec"));
71
85
 
@@ -134,11 +148,11 @@ fs.writeFileSync(sourcePath, "$ nat = <{} zero, nat succ>;\nsucc = $nat |succ $n
134
148
  const loadedSource = createState();
135
149
  output = await evaluateInput(`:load ${sourcePath}`, loadedSource);
136
150
  assert.equal(output[0], `loaded ${sourcePath}`);
137
- output = await evaluateInput(":codes", loadedSource);
151
+ output = await evaluateInput(":types", loadedSource);
138
152
  assert.match(output[0], /^nat = @/m);
139
153
  output = await evaluateInput(":rels", loadedSource);
140
154
  assert.match(output[0], /^succ = @/m);
141
- output = await evaluateInput(":d twice", loadedSource);
155
+ output = await evaluateInput(":rel twice", loadedSource);
142
156
  assert.match(output[0], /^twice = \$nat succ succ \$nat; -- @/);
143
157
  output = await evaluateInput(":C nat", loadedSource);
144
158
  assert.match(output[0], /^\$ nat = < nat succ, @[^ ]+ zero >; -- @/);
@@ -235,7 +249,7 @@ const reloaded = createState();
235
249
  output = await evaluateInput(`:load ${libPath}`, reloaded);
236
250
  assert.equal(output[0], `loaded ${libPath}`);
237
251
 
238
- output = await evaluateInput(":codes", reloaded);
252
+ output = await evaluateInput(":types", reloaded);
239
253
  assert.match(output[0], /^nat = @/m);
240
254
 
241
255
  output = await evaluateInput(":rels", reloaded);
@@ -247,7 +261,7 @@ assert.equal(output[0], "{}|succ ?<{} succ, ...>");
247
261
  const noAliasReloaded = createState();
248
262
  output = await evaluateInput(`:load --no-alias ${libPath}`, noAliasReloaded);
249
263
  assert.equal(output[0], `loaded ${libPath}`);
250
- output = await evaluateInput(":codes", noAliasReloaded);
264
+ output = await evaluateInput(":types", noAliasReloaded);
251
265
  assert.equal(output[0], "(none)");
252
266
  output = await evaluateInput(":rels", noAliasReloaded);
253
267
  assert.equal(output[0], "(none)");
@@ -255,7 +269,7 @@ assert.equal(output[0], "(none)");
255
269
  const shorthand = createState();
256
270
  output = await evaluateInput("$ nat = <{} zero, nat succ>\n; succ = |succ\n;", shorthand);
257
271
  assert.match(output[0], /\/\* comp: .* \*\//);
258
- output = await evaluateInput(":codes", shorthand);
272
+ output = await evaluateInput(":types", shorthand);
259
273
  assert.match(output[0], /^nat = @/m);
260
274
  output = await evaluateInput(":rels", shorthand);
261
275
  assert.match(output[0], /^succ = @/m);
@@ -265,7 +279,7 @@ assert.match(output[0], / false/);
265
279
  assert.match(output[0], / true/);
266
280
  output = await evaluateInput("$ maybe = <{} none, {} some>;", shorthand);
267
281
  assert.match(output[0], /\/\* comp: .* \*\//);
268
- output = await evaluateInput(":codes", shorthand);
282
+ output = await evaluateInput(":types", shorthand);
269
283
  assert.match(output[0], /^maybe = @/m);
270
284
  output = await evaluateInput("{} | true not", shorthand);
271
285
  assert.match(output[0], /^\{\}\|false \?</);
@@ -441,21 +455,75 @@ assert.equal(output[0], "loaded Examples/poly.k");
441
455
  assert.ok(polyAutoState.relAliases.reverse, "poly relations should be loaded");
442
456
  assert.ok(polyAutoState.typeAliases.int, "arithmetics int should be auto-loaded");
443
457
 
444
- // Verify timing reporting is always on and :time command
458
+ // Verify timing reporting is always on and reflects active engine
445
459
  const timingState = createState();
446
460
  assert.equal(timingState.showTiming, true);
461
+ assert.equal(timingState.engine, "wasm");
447
462
 
448
463
  output = await evaluateInput("()", timingState);
449
464
  assert.equal(output[0], "{} ?{}");
450
- assert.match(output[1], /\/\* comp: .*, exec: .* \*\//);
451
-
452
- output = await evaluateInput(":time ()", timingState);
453
- assert.equal(output[0], "{} ?{}");
454
- assert.match(output[1], /\/\* comp: .*, exec: .* \*\//);
465
+ assert.match(output[1], /\/\* comp: .*, exec: .* \(wasm\) \*\//);
455
466
  assert.ok(timingState.lastTiming);
467
+ assert.equal(timingState.lastTiming.engine, "wasm");
456
468
  assert.ok(typeof timingState.lastTiming.compileMs === "number");
457
469
  assert.ok(typeof timingState.lastTiming.executeMs === "number");
458
470
 
471
+ // Verify :engine command and switching between wasm and js engines
472
+ output = await evaluateInput(":engine", timingState);
473
+ assert.equal(output[0], "Current engine: wasm");
474
+
475
+ output = await evaluateInput(":engine js", timingState);
476
+ assert.equal(output[0], "engine set to js");
477
+ assert.equal(timingState.engine, "js");
478
+
479
+ output = await evaluateInput("()", timingState);
480
+ assert.equal(output[0], "{} ?{}");
481
+ assert.match(output[1], /\/\* comp: .*, exec: .* \(js\) \*\//);
482
+ assert.equal(timingState.lastTiming.engine, "js");
483
+
484
+ // Verify shortcuts :wasm and :js
485
+ output = await evaluateInput(":wasm", timingState);
486
+ assert.equal(output[0], "engine set to wasm");
487
+ assert.equal(timingState.engine, "wasm");
488
+
489
+ output = await evaluateInput(":js", timingState);
490
+ assert.equal(output[0], "engine set to js");
491
+ assert.equal(timingState.engine, "js");
492
+
493
+ // Verify :reset preserves the active engine
494
+ output = await evaluateInput(":reset", timingState);
495
+ assert.equal(output[0], "reset");
496
+ assert.equal(timingState.engine, "js");
497
+
498
+ // Verify invalid engine error
499
+ await assert.rejects(
500
+ () => evaluateInput(":engine invalid", timingState),
501
+ /Invalid engine 'invalid'\. Valid engines: wasm, js/
502
+ );
503
+
504
+ // Verify autocompletion for :engine
505
+ completions = completeInput(":engine ", timingState)[0];
506
+ assert(completions.includes(":engine wasm"));
507
+ assert(completions.includes(":engine js"));
508
+
509
+ // Verify evaluating expressions in JS engine
510
+ const jsState = createState({ engine: "js" });
511
+ assert.equal(jsState.engine, "js");
512
+
513
+ output = await evaluateInput("$ nat = < {} zero, nat succ >;\nsucc = | succ;\n{} | zero succ succ", jsState);
514
+ assert.match(output[0], /^\{\}\|zero\|succ\|succ/);
515
+ assert.match(output[1], /\(js\)/);
516
+
517
+ output = await evaluateInput("$ bool = < {} true, {} false >;\nnot = $bool </true | false, {} | true >;\n{} | true not", jsState);
518
+ assert.match(output[0], /^\{\}\|false/);
519
+ assert.match(output[1], /\(js\)/);
520
+
521
+ output = await evaluateInput(":load Examples/arithmetics.k", jsState);
522
+ assert.equal(output[0], "loaded Examples/arithmetics.k");
523
+ output = await evaluateInput("{ 10 int x, 5 int y } plus", jsState);
524
+ assert.match(output[0], /^\{\}\|_\|1\|1\|1\|1\|\+/);
525
+ assert.match(output[1], /\(js\)/);
526
+
459
527
  fs.rmSync(tmpDir, { recursive: true, force: true });
460
528
  fs.rmSync(symlinkPath, { force: true });
461
529
  console.log("OK");