@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 +30 -0
- package/CLAUDE.md +1 -1
- package/DOCS/BIG_PICTURE.md +3 -5
- package/DOCS/REPL.md +31 -15
- package/backends/llvm/runtime/krt.c +4 -0
- package/backends/llvm/src/kvm2llvm.mjs +2 -2
- package/backends/llvm/tests/test-llvm.mjs +19 -0
- package/backends/wasm/src/kvm2wasm.mjs +4 -0
- package/backends/wasm/tests/test-wasm.mjs +4 -0
- package/browser/repl-browser.mjs +25 -0
- package/package.json +1 -1
- package/repl.mjs +114 -94
- package/scripts/build-repl-html.mjs +27 -6
- package/tests/test-repl-html.mjs +40 -4
- package/tests/test-repl.mjs +90 -22
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 `:
|
|
64
|
+
- REPL supports commands like `:rel name` (show relation definition), `:type name` (show type definition), `:types`, `:rels`, etc.
|
package/DOCS/BIG_PICTURE.md
CHANGED
|
@@ -252,13 +252,11 @@ car = list? /cons .car;
|
|
|
252
252
|
cdr = list? /cons .cdr;
|
|
253
253
|
```
|
|
254
254
|
|
|
255
|
-
For example, the
|
|
255
|
+
For example, the definition for `car` is:
|
|
256
256
|
|
|
257
257
|
```text
|
|
258
|
-
> :
|
|
259
|
-
car
|
|
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` — displays active engine (`wasm` or `js`)
|
|
219
|
+
- `:engine wasm` (or `:wasm`) — switches to the WebAssembly engine
|
|
220
|
+
- `:engine js` (or `:js`) — 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
|
|
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
|
-
> :
|
|
278
|
-
inc
|
|
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
|
|
package/browser/repl-browser.mjs
CHANGED
|
@@ -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.
|
|
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 {
|
|
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", "
|
|
52
|
-
"
|
|
53
|
-
"
|
|
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
|
-
|
|
1051
|
-
const
|
|
1052
|
-
if (!
|
|
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
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 "
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
|
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
|
-
":
|
|
1350
|
-
":
|
|
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
|
-
":
|
|
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("
|
|
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(
|
|
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
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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::
|
|
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 & 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 <file></code></td><td>Load a <code>.k</code> source or <code>.klib</code> library into current state</td></tr>
|
|
1860
|
-
<tr><td><code>:
|
|
1861
|
-
<tr><td><code>:d <name></code></td><td>Display definition of relation <code><name></code></td></tr>
|
|
1883
|
+
<tr><td><code>:rel <name></code></td><td>Display definition of relation <code><name></code></td></tr>
|
|
1862
1884
|
<tr><td><code>:type <name></code></td><td>Display definition of type <code><name></code></td></tr>
|
|
1863
|
-
<tr><td><code>:
|
|
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 <file.mjs></code></td><td>Load a codec from file (or load through Files)</td></tr>
|
|
1867
1889
|
<tr><td><code>:codec unload <file.mjs></code></td><td>Unload a loaded codec</td></tr>
|
|
1868
1890
|
<tr><td><code>:input <codec.mjs> [text]</code></td><td>Parse input using specified codec (opens dialog if text omitted)</td></tr>
|
|
1869
|
-
<tr><td><code>:time <expr></code></td><td>Evaluate <code><expr></code> and report execution time</td></tr>
|
|
1870
1891
|
<tr><td><code>:klib <file></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>
|
package/tests/test-repl-html.mjs
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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!");
|
package/tests/test-repl.mjs
CHANGED
|
@@ -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(":
|
|
32
|
+
output = await evaluateInput(":rel succ", state);
|
|
33
33
|
assert.match(output[0], /^succ = /);
|
|
34
34
|
|
|
35
|
-
output = await evaluateInput(":
|
|
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("
|
|
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(":
|
|
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(`:
|
|
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(":
|
|
68
|
-
assert(completions.includes(":
|
|
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(":
|
|
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(":
|
|
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(":
|
|
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(":
|
|
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(":
|
|
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(":
|
|
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
|
|
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");
|