From ccc75debbd50b3a71cceadd3337037ba2ec4baab Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 15:30:22 +0000 Subject: [PATCH 1/8] examples: restore Mal 4.5 snapshot --- docs/agents/vm.md | 12 +- examples/mal/TODO.md | 95 + examples/mal/interpreter.cht | 815 ++++++++ examples/mal/step4_tests.mal | 547 ++++++ examples/minivm/README.md | 17 - examples/minivm/TODO.md | 190 -- examples/minivm/interpreter.cht | 2612 -------------------------- examples/minivm/interpreter_test.cht | 272 --- examples/minivm/run_tests.rb | 17 +- 9 files changed, 1471 insertions(+), 3106 deletions(-) create mode 100644 examples/mal/TODO.md create mode 100644 examples/mal/interpreter.cht create mode 100644 examples/mal/step4_tests.mal delete mode 100644 examples/minivm/TODO.md delete mode 100644 examples/minivm/interpreter.cht delete mode 100644 examples/minivm/interpreter_test.cht diff --git a/docs/agents/vm.md b/docs/agents/vm.md index e7942d36d..781816050 100644 --- a/docs/agents/vm.md +++ b/docs/agents/vm.md @@ -1,8 +1,12 @@ -# CLEAR VM & Gradual Typing (Scheme Backend) +# CLEAR VM & Gradual Typing (Historical Scheme Backend) + +Historical note: this document describes the older Mal/S-expression VM design. +That interpreter has been restored under `examples/mal`. The active MiniVM work +lives under `examples/minivm` and uses bytecode/register-machine paths. ## Overview -CLEAR's primary target is high-performance Zig/Native code. However, for rapid prototyping, scripting, and environments where a full compilation step is undesirable, CLEAR supports a **VM Mode**. This mode lowers CLEAR source to S-expression Scheme and executes it on a specialized interpreter written in CLEAR itself (`examples/scheme/interpreter.cht`). +CLEAR's primary target is high-performance Zig/Native code. However, for rapid prototyping, scripting, and environments where a full compilation step is undesirable, CLEAR explored a **VM Mode**. This historical mode lowered CLEAR source to S-expression Scheme and executed it on a specialized interpreter written in CLEAR itself (`examples/mal/interpreter.cht`). ## Why This Matters @@ -65,7 +69,7 @@ The GC vs. arena gap is an implementation detail invisible to the programmer. ## Current State: The Interpreter -`examples/scheme/interpreter.cht` is a Mal Level 4 implementation (~489 lines) written in CLEAR. It currently supports: +`examples/mal/interpreter.cht` is a Mal Level 4 implementation written in CLEAR. It supports: - Lexer/parser for S-expressions - `def!`, `let*`, `fn*`, `do`, `if` @@ -230,7 +234,7 @@ No `CALL_CC`. No `EVAL`. No `MACRO_EXPAND`. The opcode set is closed because the **None.** All required compiler features are already implemented. The interpreter should compile and run its 21 tests today. Verify with: ```bash -./clear test examples/scheme/interpreter.cht +./clear test examples/mal/interpreter.cht ``` ### Phase 1: Interpreter Maturation (~15-25 commits) diff --git a/examples/mal/TODO.md b/examples/mal/TODO.md new file mode 100644 index 000000000..fa641dbbb --- /dev/null +++ b/examples/mal/TODO.md @@ -0,0 +1,95 @@ +# Mal Interpreter - Status & Roadmap + +Restored from commit `12523968cdb7a871b73dfef70c5ba45b62b16b5` +(`feat: Scheme interpreter Mal 4 -> 4.5 (VM backend target)`). This is the +last Mal 4.5 snapshot that carried the Mal step 4 test file. + +This interpreter is the **confidence test** for CLEAR's v0.1-pre release +and the foundation for the **CLEAR VM backend** (see `docs/vm.md`). + +## Current Status: Mal Level 4 + +**Compiles and runs.** All 21 tests pass. The P0 blockers listed in the +original TODO are resolved - the compiler supports string escapes, charAt, +substr, toNumber, MATCH AS, @indirect, @shared, and RAISE in WHILE. + +```bash +./clear test examples/mal/interpreter.cht +``` + +Implemented: +- Lexer/parser for S-expressions +- `def!`, `let*`, `fn*`, `do`, `if` +- Lambdas with closure capture (pool-based environments) +- Arithmetic (`+`, `-`, `*`, `/`) +- Comparison (`=`, `<`, `>`, `<=`, `>=`) +- List ops (`list`, `list?`, `empty?`, `count`) +- Boolean logic (`not`), truthiness +- Recursive functions (sumdown, fibonacci) +- `prn` (readable printing) + +## Mal 4.5: VM Backend Target + +The interpreter does not need to be a full Scheme. It only needs to handle +what the CLEAR transpiler emits - a closed, known set. No `quote`, no +macros, no continuations, no `call/cc`, no `eval`, no varargs, no atoms. + +### Syntax Alignment + +The transpiler will emit standard Scheme, not Mal syntax. Either rename +the existing forms or add parallel dispatch: + +| Current (Mal) | Needed (Scheme) | Notes | +|---|---|---| +| `def!` | `define` | Top-level binding | +| `fn*` | `lambda` | Function creation | +| `let*` | `let` | Scoped bindings | +| `do` | `begin` | Sequential evaluation | +| (missing) | `set!` | Mutable binding reassignment | + +### Data Model Extensions + +CLEAR structs and unions lower to Scheme vectors and tagged pairs: + +| Work | Notes | +|------|-------| +| `vector`, `vector-ref`, `vector-set!` | STRUCT fields become vector slots | +| `cons`, `car`, `cdr` + symbol tag checks | UNION variants become `(cons 'Tag payload)` | +| String ops (`string-append`, `substring`, `string-length`, `string-ref`) | Current string support is minimal | + +### Runtime Semantics + +| Work | Size | Notes | +|------|------|-------| +| TCO: trampoline loop in `eval` | Medium | Convert tail-position calls to loop iterations. Without this, any recursive CLEAR program stack-overflows. | +| `set!` for mutable bindings | Small | Walk scope chain, find binding, update in place. | +| Error values + propagation | Large | New error Value variant. Check after every sub-eval, unwind on `RAISE`, catch on `s>`. Biggest single item. | +| Growable env pool + cycle cleanup | Medium | Replace fixed 10,000-slot array. Handle Env->Lambda->Env reference cycles. | + +### Tooling Hooks + +| Work | Size | Notes | +|------|------|-------| +| Source-map metadata | Medium | Thread CLEAR line/col through parse + eval for debugger. | +| Native function registration API | Small | Replace string `if` chain with extensible dispatch table. | +| `BREAKPOINT` hook in eval loop | Small | Check breakpoint state at each eval step. | + +### Not Needed + +These are standard Scheme/Mal features that the transpiler will never emit: + +- `call/cc` or continuations +- `quote` / `quasiquote` / macros / `macroexpand` +- `eval` at runtime +- Varargs / `& rest` +- Atoms (`atom`, `deref`, `swap!`) +- File I/O (`slurp`, `read-string`) +- Hygienic macro expansion + +## Future (Post-VM) + +- Bytecode compilation (transpiler emits bytecode directly, dispatch loop replaces tree-walker) +- Slot-indexed environments (variable index instead of hash lookup) +- Native @regex (replace manual tokenizer) +- Weak pointers for Env->Lambda->Env cycles +- Automatic @indirect inference on recursive unions diff --git a/examples/mal/interpreter.cht b/examples/mal/interpreter.cht new file mode 100644 index 000000000..47494f437 --- /dev/null +++ b/examples/mal/interpreter.cht @@ -0,0 +1,815 @@ +-- Mal (Make-a-Lisp) Interpreter in CLEAR — Pool-based edition +-- +-- Uses Env[50000]@pool for scoped environments instead of a flat HashMap. +-- Each Env holds a HashMap for its local bindings. +-- Parent links stored as Value.EnvRef in vars["__p"]. +-- Lambda closures capture envId: Id directly; body stored as Value @indirect. +-- Parser uses HashMap with numeric keys (avoids frame-arena string issues). + +STRUCT Env { + vars: HashMap +} + +UNION Value { + Nil, TrueVal, FalseVal, + Number: Float64, Str: String, Symbol: String, + List: Value[], + Vector: Value[], + Pair { pairCar: Value @indirect, pairCdr: Value @indirect }, + Lambda { params: Value[], body: Value @indirect, envId: Id }, + NativeFn: Int64, + EnvRef: Id, + Tco { tcoAst: Value @indirect, tcoEnv: Id }, + Error { errMsg: String, errKind: String } +} + +-- Pure helpers + +FN boolVal(b: Bool) RETURNS Value -> + IF b -> RETURN Value.TrueVal; + RETURN Value.FalseVal; +END + +FN isTruthy?(v: Value) RETURNS Bool -> + MATCH v START Value.Nil -> RETURN FALSE;, Value.FalseVal -> RETURN FALSE;, DEFAULT -> RETURN TRUE; END + RETURN TRUE; +END + +FN getSymName(v: Value) RETURNS String -> + MATCH v START Value.Symbol AS s -> RETURN s;, DEFAULT -> RETURN ""; END + RETURN ""; +END + +FN getNum(v: Value) RETURNS Float64 -> + MATCH v START Value.Number AS n -> RETURN n;, DEFAULT -> RETURN 0.0; END + RETURN 0.0; +END + +FN getStr(v: Value) RETURNS String -> + MATCH v START Value.Str AS s -> RETURN s;, DEFAULT -> RETURN ""; END + RETURN ""; +END + +FN getNativeId(v: Value) RETURNS Int64 -> + MATCH v START Value.NativeFn AS id -> RETURN id;, DEFAULT -> RETURN 0; END + RETURN 0; +END + +FN isList?(v: Value) RETURNS Bool -> + MATCH v START Value.List -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +FN listLen(v: Value) RETURNS Int64 -> + MATCH v START Value.List AS items -> RETURN items.length();, DEFAULT -> RETURN 0; END + RETURN 0; +END + +FN isLambda?(v: Value) RETURNS Bool -> + MATCH v START Value.Lambda -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +FN valEqual?(a: Value, b: Value) RETURNS Bool @reentrant -> + MATCH a START + Value.Nil -> MATCH b START Value.Nil -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END, + Value.TrueVal -> MATCH b START Value.TrueVal -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END, + Value.FalseVal -> MATCH b START Value.FalseVal -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END, + Value.Number AS na -> MATCH b START Value.Number AS nb -> RETURN na == nb;, DEFAULT -> RETURN FALSE; END, + Value.Str AS sa -> MATCH b START Value.Str AS sb -> RETURN sa == sb;, DEFAULT -> RETURN FALSE; END, + Value.Symbol AS sa -> MATCH b START Value.Symbol AS sb -> RETURN sa == sb;, DEFAULT -> RETURN FALSE; END, + Value.List AS la -> MATCH b START + Value.List AS lb -> + IF la.length() != lb.length() THEN RETURN FALSE; END + FOR ci IN (0_i64 ..< la.length()) DO + IF valEqual?(la[ci], lb[ci]) == FALSE THEN RETURN FALSE; END + END + RETURN TRUE;, + DEFAULT -> RETURN FALSE; END, + Value.Vector AS va -> MATCH b START + Value.Vector AS vb -> + IF va.length() != vb.length() THEN RETURN FALSE; END + FOR vi IN (0_i64 ..< va.length()) DO + IF valEqual?(va[vi], vb[vi]) == FALSE THEN RETURN FALSE; END + END + RETURN TRUE;, + DEFAULT -> RETURN FALSE; END, + Value.Pair AS pa -> MATCH b START + Value.Pair AS pb -> + RETURN valEqual?(pa.pairCar, pb.pairCar) && valEqual?(pa.pairCdr, pb.pairCdr);, + DEFAULT -> RETURN FALSE; END, + DEFAULT -> RETURN FALSE; + END + RETURN FALSE; +END + +FN readAtom(token: String) RETURNS Value -> + IF token.length() == 0 THEN RETURN Value.Nil; END + IF token == "nil" THEN RETURN Value.Nil; END + IF token == "true" THEN RETURN Value.TrueVal; END + IF token == "false" THEN RETURN Value.FalseVal; END + IF charAt(token, 0) == "\"" THEN RETURN Value{ Str: substr(token, 1, token.length() - 2) }; END + n = toNumber(token) OR (0.0 - 999999.0); + IF n != 0.0 - 999999.0 THEN RETURN Value{ Number: n }; END + RETURN Value{ Symbol: token }; +END + +FN prStr(v: Value, readably: Bool) RETURNS String @reentrant -> + MATCH v START + Value.Nil -> RETURN "nil";, + Value.TrueVal -> RETURN "true";, + Value.FalseVal -> RETURN "false";, + Value.Number AS n -> + IF n == floor(n) THEN RETURN toInt(n).toString(); END + RETURN toInt(n).toString();, + Value.Str AS s -> IF readably THEN RETURN "\"" + s + "\""; END RETURN s;, + Value.Symbol AS s -> RETURN s;, + Value.List AS items -> + MUTABLE out = "("; + FOR li IN (0_i64 ..< items.length()) DO + IF li > 0 THEN out = out + " "; END + out = out + prStr(items[li], readably); + END + RETURN out + ")";, + Value.Vector AS velems -> + MUTABLE vout = "#("; + FOR vi IN (0_i64 ..< velems.length()) DO + IF vi > 0 THEN vout = vout + " "; END + vout = vout + prStr(velems[vi], readably); + END + RETURN vout + ")";, + Value.Pair AS p -> + RETURN "(" + prStr(p.pairCar, readably) + " . " + prStr(p.pairCdr, readably) + ")";, + Value.NativeFn AS nid -> RETURN "#";, + Value.Lambda -> RETURN "#";, + Value.EnvRef -> RETURN "#";, + Value.Tco -> RETURN "#";, + Value.Error AS e -> RETURN "#"; + END + RETURN ""; +END + +-- Parser: tokenize into HashMap with numeric position tracking. +-- Tokens stored as Value.Str at keys "__t0", "__t1", etc. +-- Position and count stored as Value.Number at "__rp" and "__tc". +-- This avoids toString() inside WHILE loops for position updates. + +FN tokenizeToEnv!(MUTABLE penv: HashMap, str: String) RETURNS Void -> + MUTABLE count: Int64 = 0; + MUTABLE i: Int64 = 0; + WHILE i < str.length() DO + c = charAt(str, i); + IF c == " " || c == "," || c == "\n" || c == "\t" THEN i += 1; + ELSE_IF c == "(" || c == ")" || c == "[" || c == "]" THEN + penv["__t" + count.toString()] = Value{ Str: c }; + count += 1; + i += 1; + ELSE_IF c == ";" THEN + WHILE i < str.length() && charAt(str, i) != "\n" DO i += 1; END + ELSE_IF c == "\"" THEN + MUTABLE s = "\""; i += 1; + WHILE i < str.length() && charAt(str, i) != "\"" DO + IF charAt(str, i) == "\\" THEN + s = s + charAt(str, i); i += 1; + IF i < str.length() THEN s = s + charAt(str, i); i += 1; END + ELSE s = s + charAt(str, i); i += 1; + END + END + s = s + "\""; + penv["__t" + count.toString()] = Value{ Str: s }; + count += 1; + i += 1; + ELSE + MUTABLE s = ""; + WHILE i < str.length() && charAt(str, i) != " " && charAt(str, i) != "," && charAt(str, i) != "\n" && charAt(str, i) != "\t" && charAt(str, i) != "(" && charAt(str, i) != ")" && charAt(str, i) != "[" && charAt(str, i) != "]" && charAt(str, i) != "\"" && charAt(str, i) != ";" DO + s = s + charAt(str, i); i += 1; + END + IF s.length() > 0 THEN + penv["__t" + count.toString()] = Value{ Str: s }; + count += 1; + END + END + END + penv["__tc"] = Value{ Number: toFloat(count) }; + RETURN; +END + +FN getTokenStr!(MUTABLE penv: HashMap, idx: Int64) RETURNS String -> + val = penv["__t" + idx.toString()] OR Value.Nil; + RETURN getStr(val); +END + +FN readFormEnv!(MUTABLE penv: HashMap) RETURNS Value @reentrant -> + posVal = penv["__rp"] OR Value{ Number: 0.0 }; + tcVal = penv["__tc"] OR Value{ Number: 0.0 }; + pos = toInt(getNum(posVal)); + tc = toInt(getNum(tcVal)); + tok = getTokenStr!(penv, pos); + + IF pos >= tc THEN + RETURN Value.Nil; + END + + IF tok == "(" || tok == "[" THEN + penv["__rp"] = Value{ Number: toFloat(pos + 1) }; + RETURN readListEnv!(penv); + END + + penv["__rp"] = Value{ Number: toFloat(pos + 1) }; + RETURN readAtom(tok); +END + +FN readListEnv!(MUTABLE penv: HashMap) RETURNS Value @reentrant -> + MUTABLE items: Value[]@list = List[]; + MUTABLE listDone = FALSE; + WHILE listDone == FALSE DO + curPosVal = penv["__rp"] OR Value{ Number: 0.0 }; + tcVal2 = penv["__tc"] OR Value{ Number: 0.0 }; + curPos = toInt(getNum(curPosVal)); + tc2 = toInt(getNum(tcVal2)); + IF curPos >= tc2 THEN listDone = TRUE; + ELSE + curTok = getTokenStr!(penv, curPos); + IF curTok == ")" || curTok == "]" THEN + penv["__rp"] = Value{ Number: toFloat(curPos + 1) }; + listDone = TRUE; + ELSE + item = readFormEnv!(penv); + items.append(item); + END + END + END + RETURN Value{ List: items }; +END + +-- Native function dispatch by numeric ID. +-- IDs: 1=+ 2=- 3=* 4=/ 5== 6=< 7=> 8=<= 9=>= +-- 10=list 11=list? 12=empty? 13=count 14=not 15=prn +-- 16=vector 17=vector-ref 18=vector-set! 19=vector-length 20=vector? +-- 21=cons 22=car 23=cdr 24=pair? 25=eq? +-- 26=string-append 27=string-length 28=substring 29=string-ref +-- 30=number->string 31=string->number 32=string? 33=display + +FN applyNative(id: Int64, evaled: Value[]) RETURNS Value @reentrant -> + -- Arithmetic + IF id == 1 THEN RETURN Value{ Number: getNum(evaled[1]) + getNum(evaled[2]) }; END + IF id == 2 THEN RETURN Value{ Number: getNum(evaled[1]) - getNum(evaled[2]) }; END + IF id == 3 THEN RETURN Value{ Number: getNum(evaled[1]) * getNum(evaled[2]) }; END + IF id == 4 THEN RETURN Value{ Number: getNum(evaled[1]) / getNum(evaled[2]) }; END + -- Comparison + IF id == 5 THEN RETURN boolVal(valEqual?(evaled[1], evaled[2])); END + IF id == 6 THEN RETURN boolVal(getNum(evaled[1]) < getNum(evaled[2])); END + IF id == 7 THEN RETURN boolVal(getNum(evaled[1]) > getNum(evaled[2])); END + IF id == 8 THEN RETURN boolVal(getNum(evaled[1]) <= getNum(evaled[2])); END + IF id == 9 THEN RETURN boolVal(getNum(evaled[1]) >= getNum(evaled[2])); END + -- List + IF id == 10 THEN + MUTABLE litems: Value[]@list = List[]; + FOR li IN (1_i64 ..< evaled.length()) -> + litems.append(COPY evaled[li]); + RETURN Value{ List: litems }; + END + IF id == 11 THEN RETURN boolVal(isList?(evaled[1])); END + IF id == 12 THEN RETURN boolVal(listLen(evaled[1]) == 0); END + IF id == 13 THEN RETURN Value{ Number: toFloat(listLen(evaled[1])) }; END + IF id == 14 THEN RETURN boolVal(isTruthy?(evaled[1]) == FALSE); END + IF id == 15 THEN print(prStr(evaled[1], TRUE)); RETURN Value.Nil; END + -- Vector + IF id == 16 THEN + MUTABLE velems: Value[]@list = List[]; + FOR vi IN (1_i64 ..< evaled.length()) -> + velems.append(evaled[vi]); + RETURN Value{ Vector: velems }; + END + IF id == 17 THEN RETURN vecRef(evaled[1], toInt(getNum(evaled[2]))); END + IF id == 18 THEN RETURN Value.Nil; END + IF id == 19 THEN RETURN Value{ Number: toFloat(vecLen(evaled[1])) }; END + IF id == 20 THEN RETURN boolVal(isVector?(evaled[1])); END + -- Pair + IF id == 21 THEN RETURN Value.Pair{ pairCar: evaled[1], pairCdr: evaled[2] }; END + IF id == 22 THEN RETURN pairCar(evaled[1]); END + IF id == 23 THEN RETURN pairCdr(evaled[1]); END + IF id == 24 THEN RETURN boolVal(isPair?(evaled[1])); END + IF id == 25 THEN RETURN boolVal(valEqual?(evaled[1], evaled[2])); END + -- String + IF id == 26 THEN + MUTABLE out = getStr(evaled[1]); + FOR si IN (2_i64 ..< evaled.length()) DO + out = out + getStr(evaled[si]); + END + RETURN Value{ Str: out }; + END + IF id == 27 THEN RETURN Value{ Number: toFloat(getStr(evaled[1]).length()) }; END + IF id == 28 THEN + s = getStr(evaled[1]); + start = toInt(getNum(evaled[2])); + end_ = toInt(getNum(evaled[3])); + RETURN Value{ Str: substr(s, start, end_ - start) }; + END + IF id == 29 THEN + s = getStr(evaled[1]); + idx = toInt(getNum(evaled[2])); + RETURN Value{ Str: charAt(s, idx) }; + END + IF id == 30 THEN + n = getNum(evaled[1]); + IF n == floor(n) THEN RETURN Value{ Str: toInt(n).toString() }; END + RETURN Value{ Str: toInt(n).toString() }; + END + IF id == 31 THEN + parsed = toNumber(getStr(evaled[1])) OR (0.0 - 999999.0); + IF parsed == 0.0 - 999999.0 THEN RETURN Value.FalseVal; END + RETURN Value{ Number: parsed }; + END + IF id == 32 THEN + MATCH evaled[1] START Value.Str -> RETURN Value.TrueVal;, DEFAULT -> RETURN Value.FalseVal; END + RETURN Value.FalseVal; + END + IF id == 33 THEN print(prStr(evaled[1], FALSE)); RETURN Value.Nil; END + + RETURN Value.Nil; +END + +-- Environment operations using Pool + +FN envGet!(envId: Id, name: String, MUTABLE pool: Env[50000]@pool) RETURNS Value @reentrant -> + val = pool[envId]?.vars[name] OR Value{ Number: 0.0 - 777777.0 }; + MATCH val START + Value.Number AS n -> + IF n == 0.0 - 777777.0 THEN + parentVal = pool[envId]?.vars["__p"] OR Value.Nil; + MATCH parentVal START + Value.EnvRef AS pid -> RETURN envGet!(pid, name, pool);, + DEFAULT -> RETURN Value.Nil; + END + END + RETURN val;, + DEFAULT -> RETURN val; + END + RETURN Value.Nil; +END + +-- envSet!: walk scope chain, update existing binding. Returns TRUE if found. + +FN envSet!(envId: Id, name: String, val: Value, MUTABLE pool: Env[50000]@pool) RETURNS Bool @reentrant -> + existing = pool[envId]?.vars[name] OR Value{ Number: 0.0 - 777777.0 }; + MATCH existing START + Value.Number AS n -> + IF n == 0.0 - 777777.0 THEN + parentVal = pool[envId]?.vars["__p"] OR Value.Nil; + MATCH parentVal START + Value.EnvRef AS pid -> RETURN envSet!(pid, name, val, pool);, + DEFAULT -> RETURN FALSE; + END + END + pool[envId]?.vars[name] = val; + RETURN TRUE;, + DEFAULT -> + pool[envId]?.vars[name] = val; + RETURN TRUE; + END + RETURN FALSE; +END + +-- eval: TCO trampoline loop. Tail positions (if branches, begin/do last expr, +-- let body, lambda body) reassign ast/curEnv and continue instead of recursing. + +FN isVector?(v: Value) RETURNS Bool -> + MATCH v START Value.Vector -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +FN vecLen(v: Value) RETURNS Int64 -> + MATCH v START Value.Vector AS elems -> RETURN elems.length();, DEFAULT -> RETURN 0; END + RETURN 0; +END + +FN vecRef(v: Value, idx: Int64) RETURNS Value -> + MATCH v START + Value.Vector AS elems -> + elem = elems[idx]; + RETURN elem;, + DEFAULT -> RETURN Value.Nil; + END + RETURN Value.Nil; +END + + +FN isPair?(v: Value) RETURNS Bool -> + MATCH v START Value.Pair -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +FN pairCar(v: Value) RETURNS Value -> + MATCH v START + Value.Pair AS p -> + result = p.pairCar; + RETURN result;, + DEFAULT -> RETURN Value.Nil; + END + RETURN Value.Nil; +END + +FN pairCdr(v: Value) RETURNS Value -> + MATCH v START + Value.Pair AS p -> + result = p.pairCdr; + RETURN result;, + DEFAULT -> RETURN Value.Nil; + END + RETURN Value.Nil; +END + +FN isError?(v: Value) RETURNS Bool -> + MATCH v START Value.Error -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +FN getErrMsg(v: Value) RETURNS String -> + MATCH v START Value.Error AS e -> RETURN e.errMsg;, DEFAULT -> RETURN ""; END + RETURN ""; +END + +FN getErrKind(v: Value) RETURNS String -> + MATCH v START Value.Error AS e -> RETURN e.errKind;, DEFAULT -> RETURN ""; END + RETURN ""; +END + +FN handleCatch!(catchExpr: Value, errMsg: String, errKind: String, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value -> + MATCH catchExpr START + Value.List AS catchItems -> + catchEnvId: Id = pool.insert(Env{ vars: {} }); + pool[catchEnvId]?.vars["__p"] = Value{ EnvRef: envId }; + errBindName = getSymName(catchItems[1]); + pool[catchEnvId]?.vars[errBindName] = Value.Error{ errMsg: errMsg, errKind: errKind }; + RETURN Value.Tco{ tcoAst: catchItems[2], tcoEnv: catchEnvId };, + DEFAULT -> RETURN Value.Error{ errMsg: errMsg, errKind: errKind }; + END + RETURN Value.Error{ errMsg: errMsg, errKind: errKind }; +END + +FN isSymbol?(v: Value) RETURNS Bool -> + MATCH v START Value.Symbol -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END + RETURN FALSE; +END + +-- eval: TCO trampoline. evalList! returns Value.Tco to signal tail call. + +FN eval!(astIn: Value, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value @reentrant -> + MUTABLE ast: Value = astIn; + MUTABLE curEnv: Id = envId; + MUTABLE tcoActive = TRUE; + WHILE tcoActive DO + MATCH ast START + Value.Symbol AS sym -> + RETURN envGet!(curEnv, sym, pool);, + Value.List AS listItems -> + result = evalList!(listItems, curEnv, pool); + MATCH result START + Value.Tco AS tco -> + ast = tco.tcoAst; + curEnv = tco.tcoEnv;, + DEFAULT -> RETURN result; + END, + DEFAULT -> RETURN ast; + END + END + RETURN Value.Nil; +END + +FN evalList!(items: Value[], envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value @reentrant -> + IF items.length() == 0 THEN RETURN Value.Nil; END + formName = getSymName(items[0]); + + -- Error introspection: special forms to avoid error propagation + IF formName == "error?" THEN + val = eval!(COPY items[1], envId, pool); + RETURN boolVal(isError?(val)); + + ELSE_IF formName == "error-message" THEN + val = eval!(items[1], envId, pool); + RETURN Value{ Str: getErrMsg(val) }; + + ELSE_IF formName == "error-kind" THEN + val = eval!(items[1], envId, pool); + RETURN Value{ Str: getErrKind(val) }; + + ELSE_IF formName == "quote" THEN + quoted = items[1]; + RETURN quoted; + + ELSE_IF formName == "raise" THEN + msg = eval!(items[1], envId, pool); + IF isError?(msg) THEN RETURN msg; END + kind = eval!(items[2], envId, pool); + IF isError?(kind) THEN RETURN kind; END + RETURN Value.Error{ errMsg: getStr(msg), errKind: getStr(kind) }; + + ELSE_IF formName == "try" THEN + -- (try expr (catch e handler)) + MUTABLE tryResult: Value = eval!(items[1], envId, pool); + MATCH tryResult START + Value.Error AS e -> + RETURN handleCatch!(items[2], e.errMsg, e.errKind, envId, pool);, + DEFAULT -> RETURN tryResult; + END + RETURN tryResult; + + ELSE_IF formName == "def!" || formName == "define" THEN + defName = getSymName(items[1]); + val = eval!(items[2], envId, pool); + IF isError?(val) THEN RETURN val; END + pool[envId]?.vars[defName] = val; + result = pool[envId]?.vars[defName] OR Value.Nil; + RETURN result; + + ELSE_IF formName == "set!" THEN + setName = getSymName(items[1]); + setVal = eval!(items[2], envId, pool); + IF isError?(setVal) THEN RETURN setVal; END + envSet!(envId, setName, setVal, pool); + RETURN setVal; + + ELSE_IF formName == "let*" || formName == "let" THEN + MATCH items[1] START + Value.List AS binds -> + letId: Id = pool.insert(Env{ vars: {} }); + pool[letId]?.vars["__p"] = Value{ EnvRef: envId }; + IF binds.length() > 0 && isList?(binds[0]) THEN + FOR bi IN (0_i64 ..< binds.length()) DO + MATCH binds[bi] START + Value.List AS pair -> + bName = getSymName(pair[0]); + bVal = eval!(COPY pair[1], letId, pool); + IF isError?(bVal) THEN RETURN bVal; END + pool[letId]?.vars[bName] = bVal;, + DEFAULT -> PASS; + END + END + ELSE + MUTABLE bi: Int64 = 0; + WHILE bi < binds.length() DO + bName = getSymName(binds[bi]); + bVal = eval!(COPY binds[bi + 1], letId, pool); + IF isError?(bVal) THEN RETURN bVal; END + pool[letId]?.vars[bName] = bVal; + bi += 2; + END + END + RETURN Value.Tco{ tcoAst: COPY items[2], tcoEnv: letId };, + DEFAULT -> RETURN Value.Nil; + END + RETURN Value.Nil; + + ELSE_IF formName == "fn*" || formName == "lambda" THEN + MATCH items[1] START + Value.List AS pnames -> + RETURN Value.Lambda{ params: COPY pnames, body: COPY items[2], envId: envId };, + DEFAULT -> RETURN Value.Nil; + END + RETURN Value.Nil; + + ELSE_IF formName == "do" || formName == "begin" THEN + FOR di IN (1_i64 ..< items.length() - 1) DO + stepResult = eval!(COPY items[di], envId, pool); + IF isError?(stepResult) THEN RETURN stepResult; END + END + RETURN Value.Tco{ tcoAst: COPY items[items.length() - 1], tcoEnv: envId }; + + ELSE_IF formName == "if" THEN + cond = eval!(COPY items[1], envId, pool); + IF isError?(cond) THEN RETURN cond; END + IF isTruthy?(cond) THEN + RETURN Value.Tco{ tcoAst: COPY items[2], tcoEnv: envId }; + ELSE + IF items.length() > 3 THEN + RETURN Value.Tco{ tcoAst: COPY items[3], tcoEnv: envId }; + END + RETURN Value.Nil; + END + + ELSE + MUTABLE evaled: Value[]@list = List[]; + FOR ei IN (0_i64 ..< items.length()) DO + argVal = eval!(COPY items[ei], envId, pool); + IF isError?(argVal) THEN RETURN argVal; END + evaled.append(argVal); + END + f = evaled[0]; + IF isLambda?(f) THEN + MATCH f START + Value.Lambda AS lam -> + callId: Id = pool.insert(Env{ vars: {} }); + pool[callId]?.vars["__p"] = Value{ EnvRef: lam.envId }; + FOR pi IN (0_i64 ..< lam.params.length()) DO + pname = getSymName(lam.params[pi]); + pool[callId]?.vars[pname] = evaled[pi + 1]; + END + RETURN Value.Tco{ tcoAst: lam.body, tcoEnv: callId };, + DEFAULT -> RETURN Value.Nil; + END + ELSE + fnId = getNativeId(f); + IF fnId > 0 THEN + RETURN applyNative(fnId, evaled); + END + RETURN Value.Nil; + END + END +END + +-- runTest: tokenize + parse + eval + +FN runTest!(input: String, envId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS Value @reentrant -> + tokenizeToEnv!(penv, input); + penv["__rp"] = Value{ Number: 0.0 }; + ast = readFormEnv!(penv); + RETURN eval!(ast, envId, pool); +END + +-- Setup: create root env with all native functions registered. +-- Returns the root env Id. + +FN setupEnv!(MUTABLE pool: Env[50000]@pool) RETURNS Id -> + rootId: Id = pool.insert(Env{ vars: {} }); + -- Arithmetic: 1-4 + pool[rootId]?.vars["+"] = Value{ NativeFn: 1 }; + pool[rootId]?.vars["-"] = Value{ NativeFn: 2 }; + pool[rootId]?.vars["*"] = Value{ NativeFn: 3 }; + pool[rootId]?.vars["/"] = Value{ NativeFn: 4 }; + -- Comparison: 5-9 + pool[rootId]?.vars["="] = Value{ NativeFn: 5 }; + pool[rootId]?.vars["<"] = Value{ NativeFn: 6 }; + pool[rootId]?.vars[">"] = Value{ NativeFn: 7 }; + pool[rootId]?.vars["<="] = Value{ NativeFn: 8 }; + pool[rootId]?.vars[">="] = Value{ NativeFn: 9 }; + -- List: 10-15 + pool[rootId]?.vars["list"] = Value{ NativeFn: 10 }; + pool[rootId]?.vars["list?"] = Value{ NativeFn: 11 }; + pool[rootId]?.vars["empty?"] = Value{ NativeFn: 12 }; + pool[rootId]?.vars["count"] = Value{ NativeFn: 13 }; + pool[rootId]?.vars["not"] = Value{ NativeFn: 14 }; + pool[rootId]?.vars["prn"] = Value{ NativeFn: 15 }; + -- Vector: 16-20 + pool[rootId]?.vars["vector"] = Value{ NativeFn: 16 }; + pool[rootId]?.vars["vector-ref"] = Value{ NativeFn: 17 }; + pool[rootId]?.vars["vector-set!"] = Value{ NativeFn: 18 }; + pool[rootId]?.vars["vector-length"] = Value{ NativeFn: 19 }; + pool[rootId]?.vars["vector?"] = Value{ NativeFn: 20 }; + -- Pair: 21-24 + pool[rootId]?.vars["cons"] = Value{ NativeFn: 21 }; + pool[rootId]?.vars["car"] = Value{ NativeFn: 22 }; + pool[rootId]?.vars["cdr"] = Value{ NativeFn: 23 }; + pool[rootId]?.vars["pair?"] = Value{ NativeFn: 24 }; + -- Symbol comparison: 25 + pool[rootId]?.vars["eq?"] = Value{ NativeFn: 25 }; + -- String: 26-33 + pool[rootId]?.vars["string-append"] = Value{ NativeFn: 26 }; + pool[rootId]?.vars["string-length"] = Value{ NativeFn: 27 }; + pool[rootId]?.vars["substring"] = Value{ NativeFn: 28 }; + pool[rootId]?.vars["string-ref"] = Value{ NativeFn: 29 }; + pool[rootId]?.vars["number->string"] = Value{ NativeFn: 30 }; + pool[rootId]?.vars["string->number"] = Value{ NativeFn: 31 }; + pool[rootId]?.vars["string?"] = Value{ NativeFn: 32 }; + pool[rootId]?.vars["display"] = Value{ NativeFn: 33 }; + -- error?, error-message, error-kind are special forms (not native fns) + RETURN rootId; +END + +-- Test helpers: create fresh interpreter, run input, return result string. + +FN evalIn!(input: String, readable: Bool, rootId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS String -> + result = runTest!(input, rootId, pool, penv); + RETURN prStr(result, readable); +END + +FN evalSeqIn!(inputs: String[], readable: Bool, rootId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS String -> + MUTABLE result: Value = Value.Nil; + FOR i IN (0_i64 ..< inputs.length()) DO + result = runTest!(inputs[i], rootId, pool, penv); + END + RETURN prStr(result, readable); +END + +FN main() RETURNS Void -> + MUTABLE pool: Env[50000]@pool = []; + MUTABLE penv: HashMap = {}; + rootId = setupEnv!(pool); + + -- Arithmetic + ASSERT evalIn!("(+ 1 2)", TRUE, rootId, pool, penv) == "3", "(+ 1 2)"; + ASSERT evalIn!("(- 5 3)", TRUE, rootId, pool, penv) =="2", "(- 5 3)"; + ASSERT evalIn!("(* 3 4)", TRUE, rootId, pool, penv) =="12", "(* 3 4)"; + ASSERT evalIn!("(/ 10 2)", TRUE, rootId, pool, penv) =="5", "(/ 10 2)"; + ASSERT evalIn!("(+ (* 2 3) (- 10 4))", TRUE, rootId, pool, penv) =="12", "nested arithmetic"; + + -- Variables + ASSERT evalIn!("(def! a 10)", TRUE, rootId, pool, penv) =="10", "def! returns value"; + ASSERT evalSeqIn!(["(def! a 10)", "a"], TRUE, rootId, pool, penv) =="10", "def! is retrievable"; + + -- Closures + ASSERT evalIn!("(def! f (fn* (x) (+ x 1)))", TRUE, rootId, pool, penv) =="#", "fn* creates function"; + ASSERT evalSeqIn!(["(def! a 10)", "(def! f (fn* (x) (+ x a)))", "(f 5)"], TRUE, rootId, pool, penv) =="15", "closure captures outer"; + ASSERT evalSeqIn!(["(def! add3 (fn* (a b c) (+ a (+ b c))))", "(add3 1 2 3)"], TRUE, rootId, pool, penv) =="6", "multi-param fn"; + + -- Let bindings + ASSERT evalIn!("(let* (b 2 c 3) (+ b c))", TRUE, rootId, pool, penv) =="5", "let* scoped bindings"; + ASSERT evalIn!("(let* (a 1 b (+ a 1)) b)", TRUE, rootId, pool, penv) =="2", "let* references earlier"; + + -- Conditionals + ASSERT evalIn!("(if true 7 8)", TRUE, rootId, pool, penv) =="7", "if true"; + ASSERT evalIn!("(if false 7 8)", TRUE, rootId, pool, penv) =="8", "if false"; + ASSERT evalIn!("(if nil 7 8)", TRUE, rootId, pool, penv) =="8", "if nil"; + ASSERT evalIn!("(if false 7)", TRUE, rootId, pool, penv) =="nil", "if without else"; + + -- Sequential evaluation + ASSERT evalIn!("(do (def! d 6) 7 (+ d 8))", TRUE, rootId, pool, penv) =="14", "do returns last"; + + -- Comparison + ASSERT evalIn!("(= 1 1)", TRUE, rootId, pool, penv) =="true", "= equal"; + ASSERT evalIn!("(= 1 2)", TRUE, rootId, pool, penv) =="false", "= unequal"; + ASSERT evalIn!("(> 2 1)", TRUE, rootId, pool, penv) =="true", "> comparison"; + ASSERT evalIn!("(< 1 2)", TRUE, rootId, pool, penv) =="true", "< comparison"; + ASSERT evalIn!("(<= 2 2)", TRUE, rootId, pool, penv) =="true", "<= equal"; + ASSERT evalIn!("(>= 3 2)", TRUE, rootId, pool, penv) =="true", ">= greater"; + + -- List operations + ASSERT evalIn!("(list 1 2 3)", FALSE, rootId, pool, penv) == "(1 2 3)", "list creates list"; + ASSERT evalIn!("(count (list 1 2 3))", TRUE, rootId, pool, penv) =="3", "count"; + ASSERT evalIn!("(empty? (list))", TRUE, rootId, pool, penv) =="true", "empty?"; + ASSERT evalIn!("(not false)", TRUE, rootId, pool, penv) =="true", "not"; + ASSERT evalIn!("(list? (list 1 2))", TRUE, rootId, pool, penv) =="true", "list?"; + + -- Recursion + ASSERT evalSeqIn!(["(def! sumdown (fn* (n) (if (> n 0) (+ n (sumdown (- n 1))) 0)))", "(sumdown 6)"], TRUE, rootId, pool, penv) =="21", "sumdown"; + ASSERT evalSeqIn!(["(def! fib (fn* (n) (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))))", "(fib 4)"], TRUE, rootId, pool, penv) =="5", "fibonacci"; + + -- Scheme syntax: define, lambda, let, begin + ASSERT evalIn!("(define x 42)", TRUE, rootId, pool, penv) =="42", "define"; + ASSERT evalSeqIn!(["(define x 42)", "x"], TRUE, rootId, pool, penv) =="42", "define retrievable"; + ASSERT evalSeqIn!(["(define inc (lambda (n) (+ n 1)))", "(inc 5)"], TRUE, rootId, pool, penv) =="6", "lambda"; + ASSERT evalIn!("(let ((a 3) (b 4)) (+ a b))", TRUE, rootId, pool, penv) =="7", "let"; + ASSERT evalIn!("(begin 1 2 3)", TRUE, rootId, pool, penv) =="3", "begin"; + + -- set!: mutable reassignment + ASSERT evalSeqIn!(["(define x 1)", "(set! x 2)", "x"], TRUE, rootId, pool, penv) =="2", "set! reassigns"; + ASSERT evalSeqIn!(["(define x 10)", "(define inc! (lambda () (set! x (+ x 1))))", "(inc!)", "x"], TRUE, rootId, pool, penv) =="11", "set! in closure"; + + -- TCO: deep recursion with 50000-slot pool + ASSERT evalSeqIn!(["(define countdown (lambda (n) (if (= n 0) 0 (countdown (- n 1)))))", "(countdown 5000)"], TRUE, rootId, pool, penv) =="0", "TCO countdown 5000"; + ASSERT evalSeqIn!(["(define loop (lambda (n acc) (if (= n 0) acc (loop (- n 1) (+ acc n)))))", "(loop 2000 0)"], TRUE, rootId, pool, penv) =="2001000", "TCO accumulator 2000"; + + -- Vector operations (STRUCT lowering) + ASSERT evalIn!("(vector 10 20 30)", TRUE, rootId, pool, penv) =="#(10 20 30)", "vector create"; + ASSERT evalIn!("(vector-ref (vector 10 20 30) 0)", TRUE, rootId, pool, penv) =="10", "vector-ref 0"; + ASSERT evalIn!("(vector-ref (vector 10 20 30) 2)", TRUE, rootId, pool, penv) =="30", "vector-ref 2"; + ASSERT evalIn!("(vector-length (vector 1 2 3 4))", TRUE, rootId, pool, penv) =="4", "vector-length"; + ASSERT evalIn!("(vector? (vector 1 2))", TRUE, rootId, pool, penv) =="true", "vector? true"; + ASSERT evalIn!("(vector? (list 1 2))", TRUE, rootId, pool, penv) =="false", "vector? false on list"; + + -- Pair operations (UNION lowering) + ASSERT evalIn!("(cons 1 2)", TRUE, rootId, pool, penv) =="(1 . 2)", "cons"; + ASSERT evalIn!("(car (cons 1 2))", TRUE, rootId, pool, penv) =="1", "car"; + ASSERT evalIn!("(cdr (cons 1 2))", TRUE, rootId, pool, penv) =="2", "cdr"; + ASSERT evalIn!("(pair? (cons 1 2))", TRUE, rootId, pool, penv) =="true", "pair? true"; + ASSERT evalIn!("(pair? (list 1 2))", TRUE, rootId, pool, penv) =="false", "pair? false on list"; + + -- Tagged pairs for UNION lowering: (cons 'Tag payload) + ASSERT evalIn!("(car (cons (quote Ok) 42))", TRUE, rootId, pool, penv) =="Ok", "tagged pair car"; + ASSERT evalIn!("(cdr (cons (quote Ok) 42))", TRUE, rootId, pool, penv) =="42", "tagged pair cdr"; + ASSERT evalIn!("(eq? (car (cons (quote Err) 0)) (quote Err))", TRUE, rootId, pool, penv) =="true", "tag eq?"; + ASSERT evalIn!("(eq? (car (cons (quote Ok) 0)) (quote Err))", TRUE, rootId, pool, penv) =="false", "tag neq?"; + + -- Quote returns unevaluated + ASSERT evalIn!("(quote hello)", TRUE, rootId, pool, penv) =="hello", "quote symbol"; + ASSERT evalIn!("(quote (1 2 3))", FALSE, rootId, pool, penv) == "(1 2 3)", "quote list"; + + -- String operations + ASSERT evalIn!("(string-append \"hello\" \" \" \"world\")", TRUE, rootId, pool, penv) =="\"hello world\"", "string-append"; + ASSERT evalIn!("(string-length \"abc\")", TRUE, rootId, pool, penv) =="3", "string-length"; + ASSERT evalIn!("(substring \"abcdef\" 1 4)", TRUE, rootId, pool, penv) =="\"bcd\"", "substring"; + ASSERT evalIn!("(string-ref \"hello\" 0)", TRUE, rootId, pool, penv) =="\"h\"", "string-ref"; + ASSERT evalIn!("(number->string 42)", TRUE, rootId, pool, penv) =="\"42\"", "number->string"; + ASSERT evalIn!("(string->number \"3\")", TRUE, rootId, pool, penv) =="3", "string->number"; + ASSERT evalIn!("(string->number \"bad\")", TRUE, rootId, pool, penv) =="false", "string->number invalid"; + ASSERT evalIn!("(string? \"hi\")", TRUE, rootId, pool, penv) =="true", "string? true"; + ASSERT evalIn!("(string? 42)", TRUE, rootId, pool, penv) =="false", "string? false"; + + -- Error handling: raise + try/catch + ASSERT evalIn!("(try (raise \"boom\" \"System\") (catch e (error-message e)))", FALSE, rootId, pool, penv) =="boom", "try/catch message"; + ASSERT evalIn!("(try (raise \"x\" \"Input\") (catch e (error-kind e)))", FALSE, rootId, pool, penv) =="Input", "try/catch kind"; + ASSERT evalIn!("(try (+ 1 2) (catch e \"bad\"))", TRUE, rootId, pool, penv) =="3", "try no error"; + ASSERT evalIn!("(try (+ 1 (raise \"x\" \"E\")) (catch e \"caught\"))", FALSE, rootId, pool, penv) =="caught", "error propagates through +"; + ASSERT evalIn!("(error? (raise \"x\" \"E\"))", TRUE, rootId, pool, penv) =="true", "error? on raise"; + + -- Error propagation through define, begin, if + ASSERT evalSeqIn!(["(define safe-div (lambda (a b) (if (= b 0) (raise \"div0\" \"Input\") (/ a b))))", "(try (safe-div 10 0) (catch e \"nope\"))"], FALSE, rootId, pool, penv) =="nope", "error from lambda"; + ASSERT evalIn!("(try (begin 1 (raise \"mid\" \"E\") 3) (catch e \"stopped\"))", FALSE, rootId, pool, penv) =="stopped", "error in begin"; + ASSERT evalIn!("(try (if (raise \"cond\" \"E\") 1 2) (catch e \"cond-err\"))", FALSE, rootId, pool, penv) =="cond-err", "error in if cond"; + + -- Pool capacity: verify 50k pool handles deep programs + ASSERT evalSeqIn!(["(define deep (lambda (n) (if (= n 0) 0 (+ 1 (deep (- n 1))))))", "(deep 100)"], TRUE, rootId, pool, penv) =="100", "deep non-TCO 100"; + + print("All 75 interpreter tests PASSED!"); + RETURN; +END diff --git a/examples/mal/step4_tests.mal b/examples/mal/step4_tests.mal new file mode 100644 index 000000000..fbecb7d44 --- /dev/null +++ b/examples/mal/step4_tests.mal @@ -0,0 +1,547 @@ +;; ----------------------------------------------------- + + +;; Testing list functions +(list) +;=>() +(list? (list)) +;=>true +(list? nil) +;=>false +(empty? (list)) +;=>true +(empty? (list 1)) +;=>false +(list 1 2 3) +;=>(1 2 3) +(count (list 1 2 3)) +;=>3 +(count (list)) +;=>0 +(count nil) +;=>0 +(if (> (count (list 1 2 3)) 3) 89 78) +;=>78 +(if (>= (count (list 1 2 3)) 3) 89 78) +;=>89 + + +;; Testing if form +(if true 7 8) +;=>7 +(if false 7 8) +;=>8 +(if false 7 false) +;=>false +(if true (+ 1 7) (+ 1 8)) +;=>8 +(if false (+ 1 7) (+ 1 8)) +;=>9 +(if nil 7 8) +;=>8 +(if 0 7 8) +;=>7 +(if (list) 7 8) +;=>7 +(if (list 1 2 3) 7 8) +;=>7 +(= (list) nil) +;=>false + + +;; Testing 1-way if form +(if false (+ 1 7)) +;=>nil +(if nil 8) +;=>nil +(if nil 8 7) +;=>7 +(if true (+ 1 7)) +;=>8 + + +;; Testing basic conditionals +(= 2 1) +;=>false +(= 1 1) +;=>true +(= 1 2) +;=>false +(= 1 (+ 1 1)) +;=>false +(= 2 (+ 1 1)) +;=>true + +(> 2 1) +;=>true +(> 1 1) +;=>false +(> 1 2) +;=>false + +(>= 2 1) +;=>true +(>= 1 1) +;=>true +(>= 1 2) +;=>false + +(< 2 1) +;=>false +(< 1 1) +;=>false +(< 1 2) +;=>true + +(<= 2 1) +;=>false +(<= 1 1) +;=>true +(<= 1 2) +;=>true + + +;; Testing equality and the representation of nil false true +(= 1 1) +;=>true +(= 0 0) +;=>true +(= 1 0) +;=>false + +(= nil nil) +;=>true +(= nil false) +;=>false +(= nil true) +;=>false +(= nil 0) +;=>false +(= nil 1) +;=>false +(= nil "") +;=>false +(= nil ()) +;=>false +(= nil []) +;=>false + +(= false nil) +;=>false +(= false false) +;=>true +(= false true) +;=>false +(= false 0) +;=>false +(= false 1) +;=>false +(= false "") +;=>false +(= false ()) +;=>false + +(= true nil) +;=>false +(= true false) +;=>false +(= true true) +;=>true +(= true 0) +;=>false +(= true 1) +;=>false +(= true "") +;=>false +(= true ()) +;=>false + +(= (list) (list)) +;=>true +(= (list) ()) +;=>true +(= (list 1 2) (list 1 2)) +;=>true +(= (list 1) (list)) +;=>false +(= (list) (list 1)) +;=>false +(= 0 (list)) +;=>false +(= (list) 0) +;=>false +(= (list nil) (list)) +;=>false + + +;; Testing builtin and user defined functions +(+ 1 2) +;=>3 +( (fn* (a b) (+ b a)) 3 4) +;=>7 +( (fn* () 4) ) +;=>4 +( (fn* () ()) ) +;=>() + +( (fn* (f x) (f x)) (fn* (a) (+ 1 a)) 7) +;=>8 + + +;; Testing closures +( ( (fn* (a) (fn* (b) (+ a b))) 5) 7) +;=>12 + +(def! gen-plus5 (fn* () (fn* (b) (+ 5 b)))) +(def! plus5 (gen-plus5)) +(plus5 7) +;=>12 + +(def! gen-plusX (fn* (x) (fn* (b) (+ x b)))) +(def! plus7 (gen-plusX 7)) +(plus7 8) +;=>15 + +(let* [b 0 f (fn* [] b)] (let* [b 1] (f))) +;=>0 + +((let* [b 0] (fn* [] b))) +;=>0 + +;; Testing do form +(do (prn 101)) +;/101 +;=>nil +(do (prn 102) 7) +;/102 +;=>7 +(do (prn 101) (prn 102) (+ 1 2)) +;/101 +;/102 +;=>3 + +(do (def! a 6) 7 (+ a 8)) +;=>14 +a +;=>6 + +;; Testing special form case-sensitivity +(def! DO (fn* (a) 7)) +(DO 3) +;=>7 + +;; Testing recursive sumdown function +(def! sumdown (fn* (N) (if (> N 0) (+ N (sumdown (- N 1))) 0))) +(sumdown 1) +;=>1 +(sumdown 2) +;=>3 +(sumdown 6) +;=>21 + + +;; Testing recursive fibonacci function +(def! fib (fn* (N) (if (= N 0) 1 (if (= N 1) 1 (+ (fib (- N 1)) (fib (- N 2))))))) +(fib 1) +;=>1 +(fib 2) +;=>2 +(fib 4) +;=>5 + + +;; Testing recursive function in environment. +(let* (f (fn* () x) x 3) (f)) +;=>3 +(let* (cst (fn* (n) (if (= n 0) nil (cst (- n 1))))) (cst 1)) +;=>nil +(let* (f (fn* (n) (if (= n 0) 0 (g (- n 1)))) g (fn* (n) (f n))) (f 2)) +;=>0 + + +;>>> deferrable=True +;; +;; -------- Deferrable Functionality -------- + +;; Testing if on strings + +(if "" 7 8) +;=>7 + +;; Testing string equality + +(= "" "") +;=>true +(= "abc" "abc") +;=>true +(= "abc" "") +;=>false +(= "" "abc") +;=>false +(= "abc" "def") +;=>false +(= "abc" "ABC") +;=>false +(= (list) "") +;=>false +(= "" (list)) +;=>false + +;; Testing variable length arguments + +( (fn* (& more) (count more)) 1 2 3) +;=>3 +( (fn* (& more) (list? more)) 1 2 3) +;=>true +( (fn* (& more) (count more)) 1) +;=>1 +( (fn* (& more) (count more)) ) +;=>0 +( (fn* (& more) (list? more)) ) +;=>true +( (fn* (a & more) (count more)) 1 2 3) +;=>2 +( (fn* (a & more) (count more)) 1) +;=>0 +( (fn* (a & more) (list? more)) 1) +;=>true + + +;; Testing language defined not function +(not false) +;=>true +(not nil) +;=>true +(not true) +;=>false +(not "a") +;=>false +(not 0) +;=>false + + +;; ----------------------------------------------------- + +;; Testing string quoting + +"" +;=>"" + +"abc" +;=>"abc" + +"abc def" +;=>"abc def" + +"\"" +;=>"\"" + +"abc\ndef\nghi" +;=>"abc\ndef\nghi" + +"abc\\def\\ghi" +;=>"abc\\def\\ghi" + +"\\n" +;=>"\\n" + +;; Testing pr-str + +(pr-str) +;=>"" + +(pr-str "") +;=>"\"\"" + +(pr-str "abc") +;=>"\"abc\"" + +(pr-str "abc def" "ghi jkl") +;=>"\"abc def\" \"ghi jkl\"" + +(pr-str "\"") +;=>"\"\\\"\"" + +(pr-str (list 1 2 "abc" "\"") "def") +;=>"(1 2 \"abc\" \"\\\"\") \"def\"" + +(pr-str "abc\ndef\nghi") +;=>"\"abc\\ndef\\nghi\"" + +(pr-str "abc\\def\\ghi") +;=>"\"abc\\\\def\\\\ghi\"" + +(pr-str (list)) +;=>"()" + +;; Testing str + +(str) +;=>"" + +(str "") +;=>"" + +(str "abc") +;=>"abc" + +(str "\"") +;=>"\"" + +(str 1 "abc" 3) +;=>"1abc3" + +(str "abc def" "ghi jkl") +;=>"abc defghi jkl" + +(str "abc\ndef\nghi") +;=>"abc\ndef\nghi" + +(str "abc\\def\\ghi") +;=>"abc\\def\\ghi" + +(str (list 1 2 "abc" "\"") "def") +;=>"(1 2 abc \")def" + +(str (list)) +;=>"()" + +;; Testing prn +(prn) +;/ +;=>nil + +(prn "") +;/"" +;=>nil + +(prn "abc") +;/"abc" +;=>nil + +(prn "abc def" "ghi jkl") +;/"abc def" "ghi jkl" + +(prn "\"") +;/"\\"" +;=>nil + +(prn "abc\ndef\nghi") +;/"abc\\ndef\\nghi" +;=>nil + +(prn "abc\\def\\ghi") +;/"abc\\\\def\\\\ghi" +nil + +(prn (list 1 2 "abc" "\"") "def") +;/\(1 2 "abc" "\\""\) "def" +;=>nil + + +;; Testing println +(println) +;/ +;=>nil + +(println "") +;/ +;=>nil + +(println "abc") +;/abc +;=>nil + +(println "abc def" "ghi jkl") +;/abc def ghi jkl + +(println "\"") +;/" +;=>nil + +(println "abc\ndef\nghi") +;/abc +;/def +;/ghi +;=>nil + +(println "abc\\def\\ghi") +;/abc\\def\\ghi +;=>nil + +(println (list 1 2 "abc" "\"") "def") +;/\(1 2 abc "\) def +;=>nil + + +;; Testing keywords +(= :abc :abc) +;=>true +(= :abc :def) +;=>false +(= :abc ":abc") +;=>false +(= (list :abc) (list :abc)) +;=>true + +;; Testing vector truthiness +(if [] 7 8) +;=>7 + +;; Testing vector printing +(pr-str [1 2 "abc" "\""] "def") +;=>"[1 2 \"abc\" \"\\\"\"] \"def\"" + +(pr-str []) +;=>"[]" + +(str [1 2 "abc" "\""] "def") +;=>"[1 2 abc \"]def" + +(str []) +;=>"[]" + + +;; Testing vector functions +(count [1 2 3]) +;=>3 +(empty? [1 2 3]) +;=>false +(empty? []) +;=>true +(list? [4 5 6]) +;=>false + +;; Testing vector equality +(= [] (list)) +;=>true +(= [7 8] [7 8]) +;=>true +(= [:abc] [:abc]) +;=>true +(= (list 1 2) [1 2]) +;=>true +(= (list 1) []) +;=>false +(= [] [1]) +;=>false +(= 0 []) +;=>false +(= [] 0) +;=>false +(= [] "") +;=>false +(= "" []) +;=>false + +;; Testing vector parameter lists +( (fn* [] 4) ) +;=>4 +( (fn* [f x] (f x)) (fn* [a] (+ 1 a)) 7) +;=>8 + +;; Nested vector/list equality +(= [(list)] (list [])) +;=>true +(= [1 2 (list 3 4 [5 6])] (list 1 2 [3 4 (list 5 6)])) +;=>true diff --git a/examples/minivm/README.md b/examples/minivm/README.md index 260956043..813598abd 100644 --- a/examples/minivm/README.md +++ b/examples/minivm/README.md @@ -26,25 +26,8 @@ The active execution path is the bytecode compiler + `_bc_runner.cht`. All collection types (HashMap, @set, @list) in `_bc_runner.cht` use the native CLEAR `CheatLib.*` implementations via the same API surface as user programs. -## Deprecated: S-expression Tree-walker - -`scheme_transpiler.rb` and `interpreter.cht` are **deprecated proof-of-concept** artifacts. - -The scheme transpiler was an early PoC that walked the CLEAR AST and emitted Scheme-style -S-expressions, which the tree-walker interpreter in `interpreter.cht` then evaluated. It was -never a faithful implementation of CLEAR semantics and has been superseded by the bytecode path. - -**Do not add features to `scheme_transpiler.rb` or `interpreter.cht`.** Those files are -kept for reference only. - ## Running Tests -Primary regression check: - -```bash -./examples/minivm/clear test examples/minivm/interpreter_test.cht -``` - Run the VM test suite: ```bash diff --git a/examples/minivm/TODO.md b/examples/minivm/TODO.md deleted file mode 100644 index d71bce367..000000000 --- a/examples/minivm/TODO.md +++ /dev/null @@ -1,190 +0,0 @@ -# Mal Interpreter - Status & Roadmap - -This interpreter is the **confidence test** for CLEAR's v0.1-pre release -and the foundation for the **CLEAR VM backend** (see `docs/vm.md`). - -## Current Status: Mal Level 4 - -**Compiles and runs.** All 21 tests pass. The P0 blockers listed in the -original TODO are resolved - the compiler supports string escapes, charAt, -substr, toNumber, MATCH AS, @indirect, @shared, and RAISE in WHILE. - -```bash -./clear test examples/scheme/interpreter.cht -``` - -Implemented: -- Lexer/parser for S-expressions -- `def!`, `let*`, `fn*`, `do`, `if` -- Lambdas with closure capture (pool-based environments) -- Arithmetic (`+`, `-`, `*`, `/`) -- Comparison (`=`, `<`, `>`, `<=`, `>=`) -- List ops (`list`, `list?`, `empty?`, `count`) -- Boolean logic (`not`), truthiness -- Recursive functions (sumdown, fibonacci) -- `prn` (readable printing) - -## Mal 4.5: VM Backend Target - -The interpreter does not need to be a full Scheme. It only needs to handle -what the CLEAR transpiler emits - a closed, known set. No `quote`, no -macros, no continuations, no `call/cc`, no `eval`, no varargs, no atoms. - -### Syntax Alignment - -The transpiler will emit standard Scheme, not Mal syntax. Either rename -the existing forms or add parallel dispatch: - -| Current (Mal) | Needed (Scheme) | Notes | -|---|---|---| -| `def!` | `define` | Top-level binding | -| `fn*` | `lambda` | Function creation | -| `let*` | `let` | Scoped bindings | -| `do` | `begin` | Sequential evaluation | -| (missing) | `set!` | Mutable binding reassignment | - -### Data Model Extensions - -CLEAR structs and unions lower to Scheme vectors and tagged pairs: - -| Work | Notes | -|------|-------| -| `vector`, `vector-ref`, `vector-set!` | STRUCT fields become vector slots | -| `cons`, `car`, `cdr` + symbol tag checks | UNION variants become `(cons 'Tag payload)` | -| String ops (`string-append`, `substring`, `string-length`, `string-ref`) | Current string support is minimal | - -### Runtime Semantics - -| Work | Size | Notes | -|------|------|-------| -| TCO: trampoline loop in `eval` | Medium | Convert tail-position calls to loop iterations. Without this, any recursive CLEAR program stack-overflows. | -| `set!` for mutable bindings | Small | Walk scope chain, find binding, update in place. | -| Error values + propagation | Large | New error Value variant. Check after every sub-eval, unwind on `RAISE`, catch on `|>`. Biggest single item. | -| Growable env pool + cycle cleanup | Medium | Replace fixed 10,000-slot array. Handle Env->Lambda->Env reference cycles. | - -### Tooling Hooks - -| Work | Size | Notes | -|------|------|-------| -| Source-map metadata | Medium | Thread CLEAR line/col through parse + eval for debugger. | -| Native function registration API | Small | Replace string `if` chain with extensible dispatch table. | -| `BREAKPOINT` hook in eval loop | Small | Check breakpoint state at each eval step. | - -### Not Needed - -These are standard Scheme/Mal features that the transpiler will never emit: - -- `call/cc` or continuations -- `quote` / `quasiquote` / macros / `macroexpand` -- `eval` at runtime -- Varargs / `& rest` -- Atoms (`atom`, `deref`, `swap!`) -- File I/O (`slurp`, `read-string`) -- Hygienic macro expansion - -## Compiler Bugs (blocking clean implementation) - -1. **@list param passing** - transpiler extracts `.items` from ArrayList when passing - `MUTABLE x: Value[]@list` to a function, turning it into a slice. `.append()` fails. - Workaround: inline all @list operations, never pass @list between functions. - -2. **@list arena lifetime** - @list arrays allocated in a function's frame arena are freed - when the function returns. Storing them in HashMap entries or returning them as - Value.List causes use-after-free. PromotionPlan should promote these to the heap - when they escape through assignment or return. - Workaround: `compile!` stores bytecode entry-by-entry as Value.Number in the pool - env (inline values, no pointers). Undo this when the promotion bug is fixed - - compile! should return `Value.Pair{ List[ops], List[consts] }` directly. - -3. **MATCH AS payload return** - returning a MATCH-extracted array from a helper function - generates `items_moved = true` without declaring the tracking variable. - -4. **TEST THAT runtime pointer** - test framework passes `rt` by value instead of `&rt` - when calling functions that need the runtime from TEST THAT blocks. - -5. **Struct literal with @list fields** - `Chunk{ ops: List[], ... }` fails because the - transpiler tries to access `.items` on the error union from `makeList`. - -6. **@pool + @shared:locked composition** - `pool @shared:locked` generates incorrect Zig. - The codegen wraps the raw array type `[N]T` instead of the Pool wrapper type - `CheatLib.Pool(T)`. Should emit `Arc(Mutex(CheatLib.Pool(T)))`. Fix is in - `src/transpiler.rb` capability composition logic. This blocks real concurrency - in the interpreter - the pool can't be shared across BG fibers without it. - Workaround: sequential fake concurrency (BG evals immediately). - -7. **Early return in functions** - FIXED in Commit 27. Transpiler restructures - IF...RETURN...END patterns into nested if/else chains. - -8. **Nested list arena lifetime** - Vectors/lists inside other lists (e.g., list of structs - returned from list-index grouping) hit use-after-free when accessed after the creating - function returns. Related to bug #2 but specifically for nested collections. - Affects: 25_index field access, 29_unnest (double free). - -## Debugger Plan - -### Phase 1: File-based IPC (stub, no compiler changes) - -The interpreter uses readFile/writeFile to communicate with the Ruby wrapper: - -1. Breakpoint check in eval loop: when entering a function in the breakpoint set, - the interpreter writes env state to `_debug_env.txt` and function name to - `_debug_break.txt`, then polls `_debug_cmd.txt` for commands. -2. Ruby wrapper polls `_debug_break.txt`. When a break is detected, enters debug - REPL. User commands are written to `_debug_cmd.txt`. -3. Interpreter reads command, processes (inspect var, dump locals, eval expression), - writes response to `_debug_resp.txt`. Loops until `:continue`. -4. On `:continue`, interpreter clears break file and resumes eval. - -This gives: breakpoints, variable inspection, expression eval at break, call stack -display, continue. All without readLine. - -### Phase 2: readLine-based (requires compiler change) - -Add `readLine` to CLEAR stdlib (~10 lines of Zig): -```zig -pub fn readLine(alloc: Allocator) ![]const u8 { - return std.io.getStdIn().reader().readUntilDelimiterAlloc(alloc, '\n', 4096); -} -``` - -Replace file polling with direct stdin reading. Same debug commands, no temp files, -lower latency. The interpreter's debug REPL reads from stdin directly. - -### Phase 3: Persistent interpreter process - -Compile the interpreter ONCE with a stdin-reading main loop. The Ruby wrapper pipes -S-expressions via stdin, reads results from stdout. Process stays alive across -interactions: -- Eliminates 2-second compile latency per REPL interaction -- Eliminates arena lifetime issues (single process = persistent memory) -- Debug pausing is natural (process blocks on readLine) -- Full CLEAR runtime access maintained throughout - -## Blocked on Compiler - -**Bug #2 (@list arena lifetime) is the single biggest blocker.** Fixing PromotionPlan -to promote @list data to the heap when it escapes through @indirect assignment -would unblock ALL of the following: - -- **Typed structs**: typed-struct:i64/f64 infrastructure built, FFI functions ready, - transpiler detects homogeneous types. Data dies on return. (Commit 43) -- **Nested collections**: list-index results, struct arrays, struct-of-struct. - Inner arrays freed when outer function returns. (25_index, 29_unnest) -- **Mutation**: vector-set!, struct field mutation. MATCH AS bindings are immutable. - 7 transpile-tests blocked (04, 19, 40, 41, 42, 43, 48). -- **Bytecode compiler return**: compile! stores bytecode entry-by-entry in env - as workaround. Should return Pair{List[ops], List[consts]} directly. (Commit 9) - -## Compiler Changes Needed - -- **PromotionPlan fix** - promote @list to heap when escaping through @indirect (bug #2) -- **readLine** - add to std_lib.rb + runtime-header.zig (Phase 2+3) -- **@pool + @shared:locked** - fix capability composition for real concurrency (bug #6) - -## Future (Post-VM) - -- Bytecode compilation (transpiler emits bytecode directly, dispatch loop replaces tree-walker) -- Slot-indexed environments (variable index instead of hash lookup) -- Native @regex (replace manual tokenizer) -- Weak pointers for Env->Lambda->Env cycles -- Automatic @indirect inference on recursive unions diff --git a/examples/minivm/interpreter.cht b/examples/minivm/interpreter.cht deleted file mode 100644 index 490050aad..000000000 --- a/examples/minivm/interpreter.cht +++ /dev/null @@ -1,2612 +0,0 @@ -# Mal (Make-a-Lisp) Interpreter in CLEAR — Pool-based edition -# with typed values, debugger, and FFI bridge for typed arrays. - -REQUIRE "types.cht"; -REQUIRE "parser.cht"; -REQUIRE "debugger.cht"; - -# FFI functions: native CLEAR functions callable from Scheme via typed arrays. -# These operate on real Int64[] - no Value wrapping, no conversion. - -FN nativeSum(arr: Int64[]) RETURNS Int64 -> - MUTABLE total: Int64 = 0; - FOR i IN (0_i64 ..< arr.length()) DO - total += arr[i]; - END - RETURN total; -END - -FN nativeSumF64(arr: Float64[]) RETURNS Float64 -> - MUTABLE total: Float64 = 0.0; - FOR i IN (0_i64 ..< arr.length()) DO - total = total + arr[i]; - END - RETURN total; -END - -FN nativeDot(a: Float64[], b: Float64[]) RETURNS Float64 -> - MUTABLE total: Float64 = 0.0; - FOR i IN (0_i64 ..< a.length()) DO - total = total + a[i] * b[i]; - END - RETURN total; -END - -# FFI for typed structs: operate on raw Int64[] backing data -FN nativePointManhattan(a: Int64[], b: Int64[]) RETURNS Int64 -> - MUTABLE dx: Int64 = a[0] - b[0]; - MUTABLE dy: Int64 = a[1] - b[1]; - IF dx < 0 THEN dx = 0 - dx; END - IF dy < 0 THEN dy = 0 - dy; END - RETURN dx + dy; -END - -FN nativeTranslate(point: Int64[], dx: Int64, dy: Int64) RETURNS Int64[] -> - MUTABLE result: Int64[]@list = List[]; - result.append(point[0] + dx); - result.append(point[1] + dy); - RETURN result; -END - -FN nativeContains(arr: Int64[], needle: Int64) RETURNS Bool -> - FOR i IN (0_i64 ..< arr.length()) DO - IF arr[i] == needle THEN RETURN TRUE; END - END - RETURN FALSE; -END -# -# Uses Env[50000]@pool for scoped environments instead of a flat HashMap. -# Each Env holds a HashMap for its local bindings. -# Parent links stored as Value.EnvRef in vars["__p"]. -# Lambda closures capture envId: Id directly; body stored as Value @indirect. -# Parser uses HashMap with numeric keys (avoids frame-arena string issues). - -# Native function dispatch by numeric ID. -# IDs: 1=+ 2=- 3=* 4=/ 5== 6=< 7=> 8=<= 9=>= -# 10=list 11=list? 12=empty? 13=count 14=not 15=prn -# 16=vector 17=vector-ref 18=vector-set! 19=vector-length 20=vector? -# 21=cons 22=car 23=cdr 24=pair? 25=eq? -# 26=string-append 27=string-length 28=substring 29=string-ref -# 30=number->string 31=string->number 32=string? 33=display -# 34=list-ref 35=list-length 36=list-push 62=list-set! - -FN applyNative(id: Int64, evaled: Value[]) RETURNS Value EFFECTS REENTRANT -> - # Arithmetic - # Modulo - IF id == 37 THEN - ia = toInt(getNum(evaled[1])); - ib = toInt(getNum(evaled[2])); - MUTABLE modResult: Int64 = 0; - IF ib != 0 THEN modResult = ia MOD ib; END - RETURN Value{ Int64Val: modResult }; - END - IF id == 1 THEN - # + works on i64, f64, and strings - PARTIAL MATCH evaled[1] START - Value.Str AS s1 -> RETURN Value{ Str: s1 + getStr(evaled[2]) };, - Value.Int64Val AS ia -> - IF isInt64?(evaled[2]) THEN RETURN Value{ Int64Val: ia + getInt(evaled[2]) }; - ELSE RETURN Value{ Number: toFloat(ia) + getNum(evaled[2]) }; END, - DEFAULT -> RETURN Value{ Number: getNum(evaled[1]) + getNum(evaled[2]) }; - END - RETURN Value{ Number: getNum(evaled[1]) + getNum(evaled[2]) }; - END - IF id == 2 THEN - IF isInt64?(evaled[1]) && isInt64?(evaled[2]) THEN RETURN Value{ Int64Val: getInt(evaled[1]) - getInt(evaled[2]) }; END - RETURN Value{ Number: getNum(evaled[1]) - getNum(evaled[2]) }; - END - IF id == 3 THEN - IF isInt64?(evaled[1]) && isInt64?(evaled[2]) THEN RETURN Value{ Int64Val: getInt(evaled[1]) * getInt(evaled[2]) }; END - RETURN Value{ Number: getNum(evaled[1]) * getNum(evaled[2]) }; - END - IF id == 4 THEN - IF isInt64?(evaled[1]) && isInt64?(evaled[2]) THEN - ia = getInt(evaled[1]); - ib = getInt(evaled[2]); - IF ib == 0 THEN RETURN Value{ Int64Val: 0 }; END - RETURN Value{ Int64Val: ia / ib }; - END - RETURN Value{ Number: getNum(evaled[1]) / getNum(evaled[2]) }; - END - # Comparison - IF id == 5 THEN RETURN boolVal(valEqual?(evaled[1], evaled[2])); END - IF id == 6 THEN RETURN boolVal(getNum(evaled[1]) < getNum(evaled[2])); END - IF id == 7 THEN RETURN boolVal(getNum(evaled[1]) > getNum(evaled[2])); END - IF id == 8 THEN RETURN boolVal(getNum(evaled[1]) <= getNum(evaled[2])); END - IF id == 9 THEN RETURN boolVal(getNum(evaled[1]) >= getNum(evaled[2])); END - # List - IF id == 10 THEN - MUTABLE litems: Value[]@list = List[]; - FOR li IN (1_i64 ..< evaled.length()) -> - litems.append(COPY evaled[li]); - RETURN Value{ List: litems }; - END - IF id == 11 THEN RETURN boolVal(isList?(evaled[1])); END - IF id == 12 THEN RETURN boolVal(listLen(evaled[1]) == 0); END - IF id == 13 THEN RETURN Value{ Number: toFloat(listLen(evaled[1])) }; END - IF id == 14 THEN RETURN boolVal(isTruthy?(evaled[1]) == FALSE); END - IF id == 15 THEN print(prStr(evaled[1], TRUE)); RETURN Value.Nil; END - # Vector - IF id == 16 THEN - MUTABLE velems: Value[]@list = List[]; - FOR vi IN (1_i64 ..< evaled.length()) -> - velems.append(COPY evaled[vi]); - RETURN Value{ Vector: velems }; - END - IF id == 17 THEN RETURN vecRef(evaled[1], toInt(getNum(evaled[2]))); END - IF id == 18 THEN RETURN Value.Nil; END - IF id == 19 THEN RETURN Value{ Number: toFloat(vecLen(evaled[1])) }; END - IF id == 20 THEN RETURN boolVal(isVector?(evaled[1])); END - # Pair - IF id == 21 THEN RETURN Value.Pair{ pairCar: COPY evaled[1], pairCdr: COPY evaled[2] }; END - IF id == 22 THEN RETURN pairCar(evaled[1]); END - IF id == 23 THEN RETURN pairCdr(evaled[1]); END - IF id == 24 THEN RETURN boolVal(isPair?(evaled[1])); END - IF id == 25 THEN RETURN boolVal(valEqual?(evaled[1], evaled[2])); END - # String - IF id == 26 THEN - MUTABLE out = getStr(evaled[1]); - FOR si IN (2_i64 ..< evaled.length()) DO - out = out + getStr(evaled[si]); - END - RETURN Value{ Str: COPY out }; - END - IF id == 27 THEN RETURN Value{ Number: toFloat(getStr(evaled[1]).length()) }; END - IF id == 28 THEN - s = getStr(evaled[1]); - start = toInt(getNum(evaled[2])); - end_ = toInt(getNum(evaled[3])); - RETURN Value{ Str: substr(s, start, end_ - start) }; - END - IF id == 29 THEN - s = getStr(evaled[1]); - idx = toInt(getNum(evaled[2])); - RETURN Value{ Str: charAt(s, idx) }; - END - IF id == 30 THEN - n = getNum(evaled[1]); - IF n == floor(n) THEN RETURN Value{ Str: toInt(n).toString() }; END - RETURN Value{ Str: toInt(n).toString() }; - END - IF id == 31 THEN - parsed = toNumber(getStr(evaled[1])) OR (0.0 - 999999.0); - IF parsed == 0.0 - 999999.0 THEN RETURN Value.Error{ errMsg: "parse failed", errKind: "Input", errType: "" }; END - RETURN Value{ Number: parsed }; - END - IF id == 32 THEN - PARTIAL MATCH evaled[1] START Value.Str -> RETURN Value.TrueVal;, DEFAULT -> RETURN Value.FalseVal; END - RETURN Value.FalseVal; - END - IF id == 33 THEN print(prStr(evaled[1], FALSE)); RETURN Value.Nil; END - # String stdlib: 38=startsWith?, 39=split, 40=indexOf, 41=contains?, 42=trim, 43=charAt, 44=substr - IF id == 38 THEN - # startsWith?(str, prefix) - s = getStr(evaled[1]); - prefix = getStr(evaled[2]); - IF prefix.length() == 0 THEN RETURN Value.TrueVal; END - IF s.length() < prefix.length() THEN RETURN Value.FalseVal; END - RETURN boolVal(substr(s, 0, prefix.length()) == prefix); - END - IF id == 39 THEN - # split(str, delim) -> List of strings - s = getStr(evaled[1]); - delim = getStr(evaled[2]); - MUTABLE parts: Value[]@list = List[]; - MUTABLE start: Int64 = 0; - MUTABLE si: Int64 = 0; - WHILE si + delim.length() <= s.length() DO - IF substr(s, si, delim.length()) == delim THEN - parts.append(Value{ Str: substr(s, start, si - start) }); - si += delim.length(); - start = si; - ELSE - si += 1; - END - END - IF start <= s.length() THEN - parts.append(Value{ Str: substr(s, start, s.length() - start) }); - END - RETURN Value{ List: parts }; - END - IF id == 40 THEN - # indexOf(str, needle) -> Int64 or -1 - s = getStr(evaled[1]); - needle = getStr(evaled[2]); - MUTABLE idx: Int64 = 0; - WHILE idx <= s.length() - needle.length() DO - IF substr(s, idx, needle.length()) == needle THEN - RETURN Value{ Number: toFloat(idx) }; - END - idx += 1; - END - RETURN Value{ Number: 0.0 - 1.0 }; - END - IF id == 41 THEN - # contains?: list membership OR string substring - PARTIAL MATCH evaled[1] START - Value.List AS celems -> - FOR ci IN (0_i64 ..< celems.length()) DO - IF valEqual?(celems[ci], evaled[2]) THEN RETURN Value.TrueVal; END - END - RETURN Value.FalseVal;, - Value.TypedI64Arr AS ciarr -> - FOR ci IN (0_i64 ..< ciarr.length()) DO - IF ciarr[ci] == getInt(evaled[2]) THEN RETURN Value.TrueVal; END - END - RETURN Value.FalseVal;, - DEFAULT -> - s = getStr(evaled[1]); - needle = getStr(evaled[2]); - IF needle.length() == 0 THEN RETURN Value.TrueVal; END - MUTABLE ci: Int64 = 0; - WHILE ci + needle.length() <= s.length() DO - IF substr(s, ci, needle.length()) == needle THEN RETURN Value.TrueVal; END - ci += 1; - END - RETURN Value.FalseVal; - END - END - IF id == 42 THEN - # trim: strip leading/trailing whitespace - s = getStr(evaled[1]); - MUTABLE trimStart: Int64 = 0; - WHILE trimStart < s.length() && (charAt(s, trimStart) == " " || charAt(s, trimStart) == "\n" || charAt(s, trimStart) == "\t" || charAt(s, trimStart) == "\r") DO - trimStart += 1; - END - MUTABLE trimEnd: Int64 = s.length(); - WHILE trimEnd > trimStart && (charAt(s, trimEnd - 1) == " " || charAt(s, trimEnd - 1) == "\n" || charAt(s, trimEnd - 1) == "\t" || charAt(s, trimEnd - 1) == "\r") DO - trimEnd -= 1; - END - RETURN Value{ Str: substr(s, trimStart, trimEnd - trimStart) }; - END - IF id == 48 THEN - # endsWith?(str, suffix) - s = getStr(evaled[1]); - suffix = getStr(evaled[2]); - IF suffix.length() == 0 THEN RETURN Value.TrueVal; END - IF s.length() < suffix.length() THEN RETURN Value.FalseVal; END - RETURN boolVal(substr(s, s.length() - suffix.length(), suffix.length()) == suffix); - END - IF id == 49 THEN - # join(list, separator) -> string - MUTABLE joined = ""; - sep = getStr(evaled[2]); - PARTIAL MATCH evaled[1] START - Value.List AS joinItems -> - FOR ji IN (0_i64 ..< joinItems.length()) DO - IF ji > 0 THEN joined = joined + sep; END - joined = joined + getStr(joinItems[ji]); - END, - DEFAULT -> PASS; - END - RETURN Value{ Str: COPY joined }; - END - # Math: 50-56 - IF id == 50 THEN - v = getNum(evaled[1]); - IF v < 0.0 THEN RETURN Value{ Number: 0.0 - v }; END - RETURN Value{ Number: v }; - END - IF id == 51 THEN - # min(a, b) - a = getNum(evaled[1]); b = getNum(evaled[2]); - IF a < b THEN RETURN Value{ Number: a }; END - RETURN Value{ Number: b }; - END - IF id == 52 THEN - # max(a, b) - a = getNum(evaled[1]); b = getNum(evaled[2]); - IF a > b THEN RETURN Value{ Number: a }; END - RETURN Value{ Number: b }; - END - IF id == 53 THEN floorVal = toInt(getNum(evaled[1])); RETURN Value{ Number: toFloat(floorVal) }; END - IF id == 54 THEN ts = timestampMs(); RETURN Value{ Number: toFloat(ts) }; END - IF id == 55 THEN - RETURN Value{ Number: random() }; - END - IF id == 56 THEN - maxVal = toInt(getNum(evaled[1])); - RETURN Value{ Number: toFloat(randomInt(maxVal)) }; - END - # File I/O: 45=readFile, 46=writeFile, 47=shell - IF id == 45 THEN - path = getStr(evaled[1]); - content = readFile(path); - RETURN Value{ Str: COPY content }; - END - IF id == 46 THEN - path = getStr(evaled[1]); - content = getStr(evaled[2]); - writeFile(path, content); - RETURN Value.Nil; - END - IF id == 47 THEN - cmd = getStr(evaled[1]); - output = shell(cmd); - RETURN Value{ Str: COPY output }; - END - IF id == 44 THEN - # toInt: truncate float to integer - truncated = toInt(getNum(evaled[1])); - RETURN Value{ Number: toFloat(truncated) }; - END - IF id == 43 THEN - # substr(str, start, len) - CLEAR-style - s = getStr(evaled[1]); - start = toInt(getNum(evaled[2])); - MUTABLE len: Int64 = toInt(getNum(evaled[3])); - IF start + len > s.length() THEN len = s.length() - start; END - IF len < 0 THEN len = 0; END - RETURN Value{ Str: substr(s, start, len) }; - END - # List operations: 34=list-ref, 35=list-length - IF id == 34 THEN RETURN listRef(evaled[1], getInt(evaled[2])); END - IF id == 35 THEN - # list-length: works on lists, typed arrays, AND strings - PARTIAL MATCH evaled[1] START - Value.Str AS s -> RETURN Value{ Number: toFloat(s.length()) };, - Value.TypedI64Arr AS iarr -> RETURN Value{ Int64Val: iarr.length() };, - Value.TypedF64Arr AS farr -> RETURN Value{ Int64Val: farr.length() };, - DEFAULT -> RETURN Value{ Number: toFloat(listLen(evaled[1])) }; - END - RETURN Value{ Number: toFloat(listLen(evaled[1])) }; - END - IF id == 36 THEN - # list-push: return new list with element appended (preserves typed arrays) - PARTIAL MATCH evaled[1] START - Value.TypedI64Arr AS srcInts -> - MUTABLE newInts: Int64[]@list = List[]; - FOR li IN (0_i64 ..< srcInts.length()) DO - newInts.append(srcInts[li]); - END - newInts.append(getInt(evaled[2])); - RETURN Value{ TypedI64Arr: newInts };, - Value.TypedF64Arr AS srcFloats -> - MUTABLE newFloats: Float64[]@list = List[]; - FOR li IN (0_i64 ..< srcFloats.length()) DO - newFloats.append(srcFloats[li]); - END - newFloats.append(getNum(evaled[2])); - RETURN Value{ TypedF64Arr: newFloats };, - Value.List AS srcItems -> - MUTABLE newItems: Value[]@list = List[]; - FOR li IN (0_i64 ..< srcItems.length()) DO - newItems.append(COPY srcItems[li]); - END - newItems.append(COPY evaled[2]); - RETURN Value{ List: newItems };, - DEFAULT -> - MUTABLE singleItem: Value[]@list = List[]; - singleItem.append(COPY evaled[2]); - RETURN Value{ List: singleItem }; - END - RETURN Value.Nil; - END - - # String methods: 57=codepointCount, 58=bytes, 59=replace, 60=toUpper, 61=toLower - IF id == 57 THEN - s = getStr(evaled[1]); - RETURN Value{ Int64Val: codepointCount(s) }; - END - IF id == 58 THEN - s = getStr(evaled[1]); - RETURN Value{ Int64Val: s.length() }; - END - IF id == 59 THEN - # replace(str, old, new) - s = getStr(evaled[1]); - old = getStr(evaled[2]); - new = getStr(evaled[3]); - RETURN Value{ Str: replace(s, old, new) }; - END - IF id == 60 THEN - s = getStr(evaled[1]); - RETURN Value{ Str: uppercase(s) }; - END - IF id == 61 THEN - s = getStr(evaled[1]); - RETURN Value{ Str: lowercase(s) }; - END - IF id == 62 THEN - # list-set!: return new list with element at idx replaced (functional update) - setIdx = getInt(evaled[2]); - newElem = COPY evaled[3]; - PARTIAL MATCH evaled[1] START - Value.List AS items -> - MUTABLE newList: Value[]@list = List[]; - FOR li IN (0_i64 ..< items.length()) DO - IF li == setIdx THEN - newList.append(COPY newElem); - ELSE - newList.append(COPY items[li]); - END - END - RETURN Value{ List: newList };, - Value.TypedI64Arr AS iarr -> - MUTABLE newI64: Int64[]@list = List[]; - FOR li IN (0_i64 ..< iarr.length()) DO - IF li == setIdx THEN - newI64.append(getInt(newElem)); - ELSE - newI64.append(iarr[li]); - END - END - RETURN Value{ TypedI64Arr: newI64 };, - Value.TypedF64Arr AS farr -> - MUTABLE newF64: Float64[]@list = List[]; - FOR li IN (0_i64 ..< farr.length()) DO - IF li == setIdx THEN - newF64.append(getNum(newElem)); - ELSE - newF64.append(farr[li]); - END - END - RETURN Value{ TypedF64Arr: newF64 };, - DEFAULT -> RETURN COPY evaled[1]; - END - RETURN COPY evaled[1]; - END - IF id == 63 THEN - # set-insert: add val to set (list) if not already present - val = evaled[2]; - PARTIAL MATCH evaled[1] START - Value.List AS selems -> - FOR si IN (0_i64 ..< selems.length()) DO - IF valEqual?(selems[si], val) THEN RETURN COPY evaled[1]; END - END - MUTABLE snew: Value[]@list = List[]; - FOR si IN (0_i64 ..< selems.length()) DO snew.append(COPY selems[si]); END - snew.append(COPY val); - RETURN Value{ List: snew };, - Value.TypedI64Arr AS siarr -> - FOR si IN (0_i64 ..< siarr.length()) DO - IF siarr[si] == getInt(val) THEN RETURN COPY evaled[1]; END - END - MUTABLE sinewarr: Int64[]@list = List[]; - FOR si IN (0_i64 ..< siarr.length()) DO sinewarr.append(siarr[si]); END - sinewarr.append(getInt(val)); - RETURN Value{ TypedI64Arr: sinewarr };, - DEFAULT -> RETURN COPY evaled[1]; - END - END - IF id == 64 THEN - # set-remove: remove val from set (list) - val = evaled[2]; - PARTIAL MATCH evaled[1] START - Value.List AS relems -> - MUTABLE rnew: Value[]@list = List[]; - FOR ri IN (0_i64 ..< relems.length()) DO - IF valEqual?(relems[ri], val) == FALSE THEN rnew.append(COPY relems[ri]); END - END - RETURN Value{ List: rnew };, - Value.TypedI64Arr AS riarr -> - MUTABLE rinewarr: Int64[]@list = List[]; - FOR ri IN (0_i64 ..< riarr.length()) DO - IF riarr[ri] != getInt(val) THEN rinewarr.append(riarr[ri]); END - END - RETURN Value{ TypedI64Arr: rinewarr };, - DEFAULT -> RETURN COPY evaled[1]; - END - END - IF id == 65 THEN - # parse-i64: parse decimal string directly to Int64 (avoids float64 precision loss) - s = getStr(evaled[1]); - parsed = toInt(s) OR 0; - RETURN Value{ Int64Val: parsed }; - END - - RETURN Value.Nil; -END - -# Environment operations using Pool - -# envSet!: walk scope chain, update existing binding. Returns TRUE if found. - -FN envSet!(envId: Id, name: String, val: Value, MUTABLE pool: Env[50000]@pool) RETURNS Bool EFFECTS REENTRANT -> - IF pool[envId] AS env THEN - IF env.vars.contains?(name) THEN - env.vars[name] = COPY val; - RETURN TRUE; - END - parentVal = env.vars["__p"] OR Value.Nil; - PARTIAL MATCH parentVal START - Value.EnvRef AS pid -> RETURN envSet!(pid, name, val, pool);, - DEFAULT -> RETURN FALSE; - END - END - RETURN FALSE; -END - -# eval: TCO trampoline loop. Tail positions (if branches, begin/do last expr, -# let body, lambda body) reassign ast/curEnv and continue instead of recursing. - -FN listRef(v: Value, idx: Int64) RETURNS Value -> - PARTIAL MATCH v START - Value.List AS items -> - RETURN COPY items[idx];, - Value.TypedI64Arr AS iarr -> - RETURN Value{ Int64Val: iarr[idx] };, - Value.TypedF64Arr AS farr -> - RETURN Value{ Number: farr[idx] };, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; -END - -FN isVector?(v: Value) RETURNS Bool -> - PARTIAL MATCH v START Value.Vector -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END - RETURN FALSE; -END - -FN vecLen(v: Value) RETURNS Int64 -> - PARTIAL MATCH v START Value.Vector AS elems -> RETURN elems.length();, DEFAULT -> RETURN 0; END - RETURN 0; -END - -FN vecRef(v: Value, idx: Int64) RETURNS Value -> - PARTIAL MATCH v START - Value.Vector AS elems -> - RETURN COPY elems[idx];, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; -END - -FN vecSetSlot(v: Value, idx: Int64, newVal: Value) RETURNS Value -> - PARTIAL MATCH v START - Value.Vector AS elems -> - MUTABLE vsNew: Value[]@list = List[]; - FOR vsi IN (0_i64 ..< elems.length()) DO - IF vsi == idx THEN vsNew.append(COPY newVal); - ELSE vsNew.append(COPY elems[vsi]); END - END - RETURN Value{ Vector: vsNew };, - DEFAULT -> RETURN COPY v; - END - RETURN COPY v; -END - - -FN isPair?(v: Value) RETURNS Bool -> - PARTIAL MATCH v START Value.Pair -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END - RETURN FALSE; -END - -FN pairCar(v: Value) RETURNS Value -> - PARTIAL MATCH v START - Value.Pair AS p -> - RETURN COPY p.pairCar;, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; -END - -FN pairCdr(v: Value) RETURNS Value -> - PARTIAL MATCH v START - Value.Pair AS p -> - RETURN COPY p.pairCdr;, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; -END - -FN isTco?(v: Value) RETURNS Bool -> - PARTIAL MATCH v START Value.Tco -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END - RETURN FALSE; -END - -FN getTcoAst(v: Value) RETURNS Value -> - PARTIAL MATCH v START Value.Tco AS tco -> RETURN COPY tco.tcoAst;, DEFAULT -> RETURN Value.Nil; END - RETURN Value.Nil; -END - -FN getTcoEnv!(v: Value, MUTABLE pool: Env[50000]@pool) RETURNS Id -> - PARTIAL MATCH v START Value.Tco AS tco -> RETURN COPY tco.tcoEnv;, DEFAULT -> - dummy: Id = pool.insert(Env{ vars: {} }); - RETURN dummy; - END - dummy2: Id = pool.insert(Env{ vars: {} }); - RETURN dummy2; -END - -FN isError?(v: Value) RETURNS Bool -> - PARTIAL MATCH v START Value.Error -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END - RETURN FALSE; -END - -FN getErrMsg(v: Value) RETURNS String -> - PARTIAL MATCH v START Value.Error AS e -> RETURN COPY e.errMsg;, DEFAULT -> RETURN ""; END - RETURN ""; -END - -FN getErrKind(v: Value) RETURNS String -> - PARTIAL MATCH v START Value.Error AS e -> RETURN COPY e.errKind;, DEFAULT -> RETURN ""; END - RETURN ""; -END - -FN handleCatch!(catchExpr: Value, errMsg: String, errKind: String, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value -> - PARTIAL MATCH catchExpr START - Value.List AS catchItems -> - catchEnvId: Id = pool.insert(Env{ vars: {} }); - IF pool[catchEnvId] AS catchEnv THEN - catchEnv.vars["__p"] = Value{ EnvRef: envId }; - errBindName = getSymName(catchItems[1]); - catchEnv.vars[errBindName] = Value.Error{ errMsg: COPY errMsg, errKind: COPY errKind, errType: "" }; - END - RETURN Value.Tco{ tcoAst: COPY catchItems[2], tcoEnv: catchEnvId };, - DEFAULT -> RETURN Value.Error{ errMsg: COPY errMsg, errKind: COPY errKind, errType: "" }; - END - RETURN Value.Error{ errMsg: COPY errMsg, errKind: COPY errKind, errType: "" }; -END - -FN isSymbol?(v: Value) RETURNS Bool -> - PARTIAL MATCH v START Value.Symbol -> RETURN TRUE;, DEFAULT -> RETURN FALSE; END - RETURN FALSE; -END - -# resolveTco: if value is a Tco trampoline, evaluate it; otherwise return as-is. -FN resolveTco!(v: Value, MUTABLE pool: Env[50000]@pool) RETURNS Value EFFECTS REENTRANT -> - PARTIAL MATCH v START - Value.Tco AS tco -> - tcoAst = COPY tco.tcoAst; - tcoEnv = COPY tco.tcoEnv; - RETURN eval!(GIVE tcoAst, tcoEnv, pool);, - DEFAULT -> RETURN COPY v; - END - RETURN COPY v; -END - -# eval: TCO trampoline. evalList! returns Value.Tco to signal tail call. - -FN evalOnce!(TAKES ast: Value, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value EFFECTS REENTRANT -> - PARTIAL MATCH TAKES ast START - Value.Symbol AS sym -> - RETURN envGet!(envId, sym, pool);, - Value.List AS listItems -> - ownedItems: Value[] = GIVE listItems; - RETURN evalList!(ownedItems, envId, pool);, - Value.Nil -> RETURN Value.Nil;, - Value.TrueVal -> RETURN Value.TrueVal;, - Value.FalseVal -> RETURN Value.FalseVal;, - Value.Number AS n -> RETURN Value{ Number: n };, - Value.Int64Val AS i -> RETURN Value{ Int64Val: i };, - Value.Str AS s -> RETURN Value{ Str: COPY s };, - Value.NativeFn AS id -> RETURN Value{ NativeFn: id };, - Value.Error AS e -> RETURN Value.Error{ errMsg: COPY e.errMsg, errKind: COPY e.errKind, errType: "" };, - Value.Lambda AS lam -> RETURN Value.Lambda{ params: COPY lam.params, body: COPY lam.body, envId: lam.envId };, - Value.Vector AS vec -> RETURN Value{ Vector: vec };, - Value.TypedI64Arr AS iarr -> RETURN Value{ TypedI64Arr: iarr };, - Value.TypedF64Arr AS farr -> RETURN Value{ TypedF64Arr: farr };, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; -END - -FN eval!(TAKES ast: Value, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value EFFECTS REENTRANT -> - MUTABLE result: Value = evalOnce!(GIVE ast, envId, pool); - MUTABLE bouncing = isTco?(result); - WHILE bouncing DO - tcoAst = getTcoAst(result); - tcoEnv = getTcoEnv!(result, pool); - result = evalOnce!(tcoAst, tcoEnv, pool); - bouncing = isTco?(result); - END - RETURN result; -END - -FN evalList!(TAKES items: Value[], envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value EFFECTS REENTRANT -> - IF items.length() == 0 THEN RETURN Value.Nil; END - formName = getSymName(items[0]); - - # Pipeline operations: special forms that call lambdas - IF formName == "list-where" THEN - # (list-where list pred) -> filter (handles List and TypedI64Arr) - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - predVal = eval!(COPY items[2], envId, pool); - MUTABLE filtered: Value[]@list = List[]; - len = listLen(listVal); - FOR fi IN (0_i64 ..< len) DO - elem = listRef(listVal, fi); - MUTABLE callArgs: Value[]@list = List[]; - callArgs.append(COPY predVal); - callArgs.append(COPY elem); - MUTABLE callResult: Value = evalList!(callArgs, envId, pool); - callResult = resolveTco!(callResult, pool); - IF isTruthy?(callResult) THEN filtered.append(COPY elem); END - END - RETURN Value{ List: filtered }; - - ELSE_IF formName == "list-select" THEN - # (list-select list fn) -> map (handles List, TypedI64Arr, TypedF64Arr) - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - fnVal = eval!(COPY items[2], envId, pool); - MUTABLE mapped: Value[]@list = List[]; - mlen = listLen(listVal); - FOR mi IN (0_i64 ..< mlen) DO - MUTABLE callArgs: Value[]@list = List[]; - callArgs.append(COPY fnVal); - callArgs.append(listRef(listVal, mi)); - MUTABLE callResult: Value = evalList!(callArgs, envId, pool); - callResult = resolveTco!(callResult, pool); - mapped.append(callResult); - END - RETURN Value{ List: mapped }; - - ELSE_IF formName == "list-reduce" THEN - # (list-reduce list init fn) -> fold (handles both List and TypedI64Arr) - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - MUTABLE acc: Value = eval!(COPY items[2], envId, pool); - fnVal = eval!(COPY items[3], envId, pool); - len = listLen(listVal); - FOR ri IN (0_i64 ..< len) DO - elem = listRef(listVal, ri); - MUTABLE callArgs: Value[]@list = List[]; - callArgs.append(COPY fnVal); - callArgs.append(COPY acc); - callArgs.append(COPY elem); - MUTABLE callResult: Value = evalList!(callArgs, envId, pool); - acc = resolveTco!(callResult, pool); - END - RETURN acc; - - ELSE_IF formName == "list-limit" THEN - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - limitN = getInt(eval!(COPY items[2], envId, pool)); - len = listLen(listVal); - MUTABLE limited: Value[]@list = List[]; - FOR li IN (0_i64 ..< len) DO - IF li < limitN THEN - elem = listRef(listVal, li); - limited.append(COPY elem); - END - END - RETURN Value{ List: limited }; - - ELSE_IF formName == "list-distinct" THEN - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - MUTABLE distinct: Value[]@list = List[]; - len = listLen(listVal); - FOR di IN (0_i64 ..< len) DO - elem = listRef(listVal, di); - MUTABLE found = FALSE; - FOR dj IN (0_i64 ..< distinct.length()) DO - IF valEqual?(elem, distinct[dj]) THEN found = TRUE; END - END - IF found == FALSE THEN distinct.append(COPY elem); END - END - RETURN Value{ List: distinct }; - - ELSE_IF formName == "list-orderby" THEN - # (list-orderby list keyFn) -> sorted copy (insertion sort) - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE sorted: Value[]@list = List[]; - len = listLen(listVal); - FOR oi IN (0_i64 ..< len) DO - elem = listRef(listVal, oi); - sorted.append(COPY elem); - END - # Insertion sort by key - MUTABLE si: Int64 = 1; - IF sorted.length() > 0 THEN - WHILE si < sorted.length() DO - MUTABLE j: Int64 = si; - WHILE j > 0 DO - MUTABLE aArgs: Value[]@list = List[]; - aArgs.append(COPY keyFn); aArgs.append(COPY sorted[j]); - MUTABLE aKey: Value = evalList!(aArgs, envId, pool); - aKey = resolveTco!(aKey, pool); - MUTABLE bArgs: Value[]@list = List[]; - bArgs.append(COPY keyFn); bArgs.append(COPY sorted[j - 1]); - MUTABLE bKey: Value = evalList!(bArgs, envId, pool); - bKey = resolveTco!(bKey, pool); - IF getNum(aKey) < getNum(bKey) THEN - tmp = COPY sorted[j - 1]; - sorted[j - 1] = COPY sorted[j]; - sorted[j] = tmp; - j -= 1; - ELSE - j = 0; - END - END - si += 1; - END - END - RETURN Value{ List: sorted }; - - ELSE_IF formName == "list-unnest" THEN - # (list-unnest list fieldFn) -> flatten nested lists - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - fieldFn = eval!(COPY items[2], envId, pool); - MUTABLE flat: Value[]@list = List[]; - PARTIAL MATCH listVal START - Value.List AS srcItems -> - FOR ui IN (0_i64 ..< srcItems.length()) DO - MUTABLE fArgs: Value[]@list = List[]; - fArgs.append(COPY fieldFn); fArgs.append(COPY srcItems[ui]); - MUTABLE nested: Value = evalList!(fArgs, envId, pool); - nested = resolveTco!(nested, pool); - PARTIAL MATCH nested START - Value.List AS innerItems -> - FOR ni IN (0_i64 ..< innerItems.length()) DO - flat.append(COPY innerItems[ni]); - END, - Value.TypedI64Arr AS innerI64 -> - FOR ni IN (0_i64 ..< innerI64.length()) DO - flat.append(Value{ Int64Val: innerI64[ni] }); - END, - Value.TypedF64Arr AS innerF64 -> - FOR ni IN (0_i64 ..< innerF64.length()) DO - flat.append(Value{ Number: innerF64[ni] }); - END, - DEFAULT -> flat.append(nested); - END - END, - DEFAULT -> PASS; - END - RETURN Value{ List: flat }; - - ELSE_IF formName == "list-index" THEN - # (list-index list keyFn) -> assoc list of (key . items[]) - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE groups: Value[]@list = List[]; - PARTIAL MATCH listVal START - Value.List AS srcItems -> - FOR gi IN (0_i64 ..< srcItems.length()) DO - MUTABLE kArgs: Value[]@list = List[]; - kArgs.append(COPY keyFn); kArgs.append(COPY srcItems[gi]); - MUTABLE gKey: Value = evalList!(kArgs, envId, pool); - gKey = resolveTco!(gKey, pool); - # Find existing group - MUTABLE found: Int64 = 0 - 1; - FOR fi IN (0_i64 ..< groups.length()) DO - groupPair = pairCar(groups[fi]); - IF valEqual?(groupPair, gKey) THEN found = fi; END - END - IF found >= 0 THEN - # Add to existing group (rebuild pair with appended list) - existingList = pairCdr(groups[found]); - MUTABLE newGroupItems: Value[]@list = List[]; - PARTIAL MATCH existingList START - Value.List AS gl -> FOR gj IN (0_i64 ..< gl.length()) DO newGroupItems.append(COPY gl[gj]); END, - DEFAULT -> PASS; - END - newGroupItems.append(COPY srcItems[gi]); - groups[found] = Value.Pair{ pairCar: COPY gKey, pairCdr: Value{ List: newGroupItems } }; - ELSE - # New group - MUTABLE newItems: Value[]@list = List[]; - newItems.append(COPY srcItems[gi]); - groups.append(Value.Pair{ pairCar: COPY gKey, pairCdr: Value{ List: newItems } }); - END - END, - DEFAULT -> PASS; - END - RETURN Value{ List: groups }; - - ELSE_IF formName == "assoc-get" THEN - # (assoc-get alist key) -> value or error if not found - alist = eval!(COPY items[1], envId, pool); - key = eval!(COPY items[2], envId, pool); - PARTIAL MATCH alist START - Value.List AS pairs -> - FOR ai IN (0_i64 ..< pairs.length()) DO - pKey = pairCar(pairs[ai]); - IF valEqual?(pKey, key) THEN - RETURN pairCdr(pairs[ai]); - END - END, - DEFAULT -> PASS; - END - RETURN Value.Error{ errMsg: "key not found", errKind: "NotFound", errType: "" }; - - ELSE_IF formName == "list-slice" THEN - listVal = eval!(COPY items[1], envId, pool); - startIdx = getInt(eval!(COPY items[2], envId, pool)); - endIdx = getInt(eval!(COPY items[3], envId, pool)); - PARTIAL MATCH listVal START - Value.List AS srcItems -> - MUTABLE sliced: Value[]@list = List[]; - FOR si IN (startIdx ..= endIdx) DO - IF si < srcItems.length() THEN sliced.append(COPY srcItems[si]); END - END - RETURN Value{ List: sliced };, - Value.TypedI64Arr AS iarr -> - MUTABLE isliced: Value[]@list = List[]; - FOR si IN (startIdx ..= endIdx) DO - IF si < iarr.length() THEN isliced.append(Value{ Int64Val: iarr[si] }); END - END - RETURN Value{ List: isliced };, - Value.TypedF64Arr AS farr -> - MUTABLE fsliced: Value[]@list = List[]; - FOR si IN (startIdx ..= endIdx) DO - IF si < farr.length() THEN fsliced.append(Value{ Number: farr[si] }); END - END - RETURN Value{ List: fsliced };, - Value.Str AS str -> - sliceLen = endIdx - startIdx + 1; - RETURN Value{ Str: substr(str, startIdx, sliceLen) };, - DEFAULT -> - MUTABLE emptyDef: Value[]@list = List[]; - RETURN Value{ List: emptyDef }; - END - MUTABLE emptySlice: Value[]@list = List[]; - RETURN Value{ List: emptySlice }; - - ELSE_IF formName == "list-range" THEN - startVal = toInt(getNum(eval!(COPY items[1], envId, pool))); - endVal = toInt(getNum(eval!(COPY items[2], envId, pool))); - MUTABLE rangeItems: Value[]@list = List[]; - FOR ri IN (startVal ..< endVal) DO - rangeItems.append(Value{ Number: toFloat(ri) }); - END - RETURN Value{ List: rangeItems }; - - ELSE_IF formName == "toList" THEN - # Convert range/list to list (identity for lists, used with stream types) - IF items.length() > 1 THEN - tlVal = eval!(COPY items[1], envId, pool); - RETURN tlVal; - END - RETURN Value.Nil; - - ELSE_IF formName == "list-count" THEN - IF items.length() > 2 THEN - countListVal = eval!(COPY items[1], envId, pool); - predFn = eval!(COPY items[2], envId, pool); - MUTABLE cnt: Int64 = 0; - cntLen = listLen(countListVal); - FOR si IN (0_i64 ..< cntLen) DO - MUTABLE pArgs: Value[]@list = List[]; - pArgs.append(COPY predFn); pArgs.append(listRef(countListVal, si)); - MUTABLE pResult: Value = evalList!(pArgs, envId, pool); - pResult = resolveTco!(pResult, pool); - IF isTruthy?(pResult) THEN cnt += 1; END - END - RETURN Value{ Int64Val: cnt }; - END - countListVal2 = eval!(COPY items[1], envId, pool); - RETURN Value{ Int64Val: listLen(countListVal2) }; - - ELSE_IF formName == "list-sum" THEN - listVal = eval!(COPY items[1], envId, pool); - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE total = 0.0; - sumLen = listLen(listVal); - FOR si IN (0_i64 ..< sumLen) DO - MUTABLE kArgs: Value[]@list = List[]; - kArgs.append(COPY keyFn); kArgs.append(listRef(listVal, si)); - MUTABLE kResult: Value = evalList!(kArgs, envId, pool); - kResult = resolveTco!(kResult, pool); - total = total + getNum(kResult); - END - RETURN Value{ Number: total }; - - ELSE_IF formName == "list-avg" THEN - listVal = eval!(COPY items[1], envId, pool); - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE total = 0.0; - count = listLen(listVal); - FOR si IN (0_i64 ..< count) DO - MUTABLE kArgs: Value[]@list = List[]; - kArgs.append(COPY keyFn); kArgs.append(listRef(listVal, si)); - MUTABLE kResult: Value = evalList!(kArgs, envId, pool); - kResult = resolveTco!(kResult, pool); - total = total + getNum(kResult); - END - IF count > 0 THEN RETURN Value{ Number: total / toFloat(count) }; END - RETURN Value{ Number: 0.0 }; - - ELSE_IF formName == "list-min" THEN - listVal = eval!(COPY items[1], envId, pool); - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE best = 999999999.0; - minLen = listLen(listVal); - FOR si IN (0_i64 ..< minLen) DO - MUTABLE kArgs: Value[]@list = List[]; - kArgs.append(COPY keyFn); kArgs.append(listRef(listVal, si)); - MUTABLE kResult: Value = evalList!(kArgs, envId, pool); - kResult = resolveTco!(kResult, pool); - v = getNum(kResult); - IF v < best THEN best = v; END - END - RETURN Value{ Number: best }; - - ELSE_IF formName == "list-max" THEN - listVal = eval!(COPY items[1], envId, pool); - keyFn = eval!(COPY items[2], envId, pool); - MUTABLE best = 0.0 - 999999999.0; - maxLen = listLen(listVal); - FOR si IN (0_i64 ..< maxLen) DO - MUTABLE kArgs: Value[]@list = List[]; - kArgs.append(COPY keyFn); kArgs.append(listRef(listVal, si)); - MUTABLE kResult: Value = evalList!(kArgs, envId, pool); - kResult = resolveTco!(kResult, pool); - v = getNum(kResult); - IF v > best THEN best = v; END - END - RETURN Value{ Number: best }; - - ELSE_IF formName == "list-find" THEN - listVal = eval!(COPY items[1], envId, pool); - predFn = eval!(COPY items[2], envId, pool); - len = listLen(listVal); - FOR si IN (0_i64 ..< len) DO - elem = listRef(listVal, si); - MUTABLE pArgs: Value[]@list = List[]; - pArgs.append(COPY predFn); pArgs.append(COPY elem); - MUTABLE pResult: Value = evalList!(pArgs, envId, pool); - pResult = resolveTco!(pResult, pool); - IF isTruthy?(pResult) THEN RETURN COPY elem; END - END - RETURN Value.Nil; - - ELSE_IF formName == "list-any" THEN - listVal = eval!(COPY items[1], envId, pool); - predFn = eval!(COPY items[2], envId, pool); - anyLen = listLen(listVal); - FOR si IN (0_i64 ..< anyLen) DO - MUTABLE pArgs: Value[]@list = List[]; - pArgs.append(COPY predFn); pArgs.append(listRef(listVal, si)); - MUTABLE pResult: Value = evalList!(pArgs, envId, pool); - pResult = resolveTco!(pResult, pool); - IF isTruthy?(pResult) THEN RETURN Value.TrueVal; END - END - RETURN Value.FalseVal; - - ELSE_IF formName == "list-all" THEN - listVal = eval!(COPY items[1], envId, pool); - predFn = eval!(COPY items[2], envId, pool); - allLen = listLen(listVal); - FOR si IN (0_i64 ..< allLen) DO - MUTABLE pArgs: Value[]@list = List[]; - pArgs.append(COPY predFn); pArgs.append(listRef(listVal, si)); - MUTABLE pResult: Value = evalList!(pArgs, envId, pool); - pResult = resolveTco!(pResult, pool); - IF isTruthy?(pResult) == FALSE THEN RETURN Value.FalseVal; END - END - RETURN Value.TrueVal; - - ELSE_IF formName == "list-each" THEN - listVal = eval!(COPY items[1], envId, pool); - fnVal = eval!(COPY items[2], envId, pool); - eachLen = listLen(listVal); - FOR si IN (0_i64 ..< eachLen) DO - MUTABLE eArgs: Value[]@list = List[]; - eArgs.append(COPY fnVal); eArgs.append(listRef(listVal, si)); - MUTABLE eResult: Value = evalList!(eArgs, envId, pool); - eResult = resolveTco!(eResult, pool); - END - RETURN listVal; - - ELSE_IF formName == "list-skip" THEN - listVal = eval!(COPY items[1], envId, pool); - skipN = getInt(eval!(COPY items[2], envId, pool)); - MUTABLE skipped: Value[]@list = List[]; - skipLen = listLen(listVal); - FOR si IN (skipN ..< skipLen) DO - skipped.append(listRef(listVal, si)); - END - RETURN Value{ List: skipped }; - - ELSE_IF formName == "list-take-while" THEN - listVal = eval!(COPY items[1], envId, pool); - predFn = eval!(COPY items[2], envId, pool); - MUTABLE taken: Value[]@list = List[]; - twLen = listLen(listVal); - FOR si IN (0_i64 ..< twLen) DO - elem = listRef(listVal, si); - MUTABLE tArgs: Value[]@list = List[]; - tArgs.append(COPY predFn); tArgs.append(COPY elem); - MUTABLE tResult: Value = evalList!(tArgs, envId, pool); - tResult = resolveTco!(tResult, pool); - IF isTruthy?(tResult) THEN taken.append(COPY elem); - ELSE BREAK; END - END - RETURN Value{ List: taken }; - - ELSE_IF formName == "assoc-set" THEN - # (assoc-set alist key val) -> new alist with key set - alist = eval!(COPY items[1], envId, pool); - key = eval!(COPY items[2], envId, pool); - IF isError?(key) THEN RETURN key; END - val = eval!(COPY items[3], envId, pool); - IF isError?(val) THEN RETURN val; END - MUTABLE newPairs: Value[]@list = List[]; - MUTABLE replaced = FALSE; - PARTIAL MATCH alist START - Value.TypedI64Arr AS iarr -> - MUTABLE newIarr: Int64[]@list = List[]; - FOR asi IN (0_i64 ..< iarr.length()) DO - IF asi == getInt(key) THEN newIarr.append(getInt(val)); - ELSE newIarr.append(iarr[asi]); END - END - RETURN Value{ TypedI64Arr: newIarr };, - Value.List AS pairs -> - FOR ai IN (0_i64 ..< pairs.length()) DO - pKey = pairCar(pairs[ai]); - IF valEqual?(pKey, key) THEN - newPairs.append(Value.Pair{ pairCar: COPY key, pairCdr: COPY val }); - replaced = TRUE; - ELSE - newPairs.append(COPY pairs[ai]); - END - END, - DEFAULT -> PASS; - END - IF replaced == FALSE THEN - newPairs.append(Value.Pair{ pairCar: COPY key, pairCdr: COPY val }); - END - RETURN Value{ List: newPairs }; - - ELSE_IF formName == "assoc-delete" THEN - # (assoc-delete alist key) -> new alist without key - alist = eval!(COPY items[1], envId, pool); - key = eval!(COPY items[2], envId, pool); - MUTABLE adPairs: Value[]@list = List[]; - PARTIAL MATCH alist START - Value.List AS pairs -> - FOR ai IN (0_i64 ..< pairs.length()) DO - adKey = pairCar(pairs[ai]); - IF valEqual?(adKey, key) == FALSE THEN adPairs.append(COPY pairs[ai]); END - END, - DEFAULT -> PASS; - END - RETURN Value{ List: adPairs }; - - ELSE_IF formName == "assoc-contains?" THEN - # (assoc-contains? alist key) -> bool - alist = eval!(COPY items[1], envId, pool); - key = eval!(COPY items[2], envId, pool); - PARTIAL MATCH alist START - Value.List AS pairs -> - FOR ai IN (0_i64 ..< pairs.length()) DO - acKey = pairCar(pairs[ai]); - IF valEqual?(acKey, key) THEN RETURN Value.TrueVal; END - END, - DEFAULT -> PASS; - END - RETURN Value.FalseVal; - - # Debug introspection - ELSE_IF formName == "env-keys" THEN - RETURN Value{ Str: "(use :inspect to check specific variables)" }; - - ELSE_IF formName == "type-of" THEN - # (type-of expr) -> string describing the type - val = eval!(COPY items[1], envId, pool); - PARTIAL MATCH val START - Value.Nil -> RETURN Value{ Str: "Nil" };, - Value.TrueVal -> RETURN Value{ Str: "Bool" };, - Value.FalseVal -> RETURN Value{ Str: "Bool" };, - Value.Number -> RETURN Value{ Str: "Float64" };, - Value.Int64Val -> RETURN Value{ Str: "Int64" };, - Value.Str -> RETURN Value{ Str: "String" };, - Value.Symbol -> RETURN Value{ Str: "Symbol" };, - Value.List -> RETURN Value{ Str: "List" };, - Value.Vector -> RETURN Value{ Str: "Vector" };, - Value.Pair -> RETURN Value{ Str: "Pair" };, - Value.Lambda -> RETURN Value{ Str: "Function" };, - Value.NativeFn -> RETURN Value{ Str: "NativeFunction" };, - Value.Error -> RETURN Value{ Str: "Error" };, - DEFAULT -> RETURN Value{ Str: "Unknown" }; - END - RETURN Value{ Str: "Unknown" }; - - ELSE_IF formName == "typed-list:i64" THEN - # (typed-list:i64 1 2 3) -> TypedI64Arr with raw Int64[] storage - MUTABLE typedItems: Int64[]@list = List[]; - FOR ti IN (1_i64 ..< items.length()) DO - tval = eval!(COPY items[ti], envId, pool); - IF isError?(tval) THEN RETURN tval; END - typedItems.append(getInt(tval)); - END - RETURN Value{ TypedI64Arr: typedItems }; - - # Typed struct: (cons 'Tag TypedI64Arr/TypedF64Arr/Vector) - ELSE_IF formName == "typed-struct:i64" THEN - # (typed-struct:i64 "Name" field1 field2 ...) -> Pair{Symbol(Name), TypedI64Arr} - tagVal = eval!(COPY items[1], envId, pool); - IF isError?(tagVal) THEN RETURN tagVal; END - MUTABLE structData: Int64[]@list = List[]; - FOR fi IN (2_i64 ..< items.length()) DO - fval = eval!(COPY items[fi], envId, pool); - IF isError?(fval) THEN RETURN fval; END - structData.append(getInt(fval)); - END - RETURN Value.Pair{ pairCar: Value{ Symbol: getStr(tagVal) }, pairCdr: Value{ TypedI64Arr: structData } }; - - ELSE_IF formName == "typed-struct:f64" THEN - tagVal = eval!(COPY items[1], envId, pool); - IF isError?(tagVal) THEN RETURN tagVal; END - MUTABLE structData: Float64[]@list = List[]; - FOR fi IN (2_i64 ..< items.length()) DO - fval = eval!(COPY items[fi], envId, pool); - IF isError?(fval) THEN RETURN fval; END - structData.append(getNum(fval)); - END - RETURN Value.Pair{ pairCar: Value{ Symbol: getStr(tagVal) }, pairCdr: Value{ TypedF64Arr: structData } }; - - ELSE_IF formName == "typed-struct-ref:i64" THEN - sval = eval!(COPY items[1], envId, pool); - IF isError?(sval) THEN RETURN sval; END - idx = getInt(eval!(COPY items[2], envId, pool)); - dataVal = pairCdr(sval); - RETURN listRef(dataVal, idx); - - ELSE_IF formName == "typed-struct-ref:f64" THEN - sval = eval!(COPY items[1], envId, pool); - IF isError?(sval) THEN RETURN sval; END - idx = getInt(eval!(COPY items[2], envId, pool)); - dataVal = pairCdr(sval); - RETURN listRef(dataVal, idx); - - ELSE_IF formName == "typed-struct-ref:mixed" THEN - sval = eval!(COPY items[1], envId, pool); - IF isError?(sval) THEN RETURN sval; END - idx = getInt(eval!(COPY items[2], envId, pool)); - RETURN vecRef(sval, idx); - - ELSE_IF formName == "typed-list:f64" THEN - MUTABLE typedFloats: Float64[]@list = List[]; - FOR ti IN (1_i64 ..< items.length()) DO - tval = eval!(COPY items[ti], envId, pool); - IF isError?(tval) THEN RETURN tval; END - typedFloats.append(getNum(tval)); - END - RETURN Value{ TypedF64Arr: typedFloats }; - - ELSE_IF formName == "to-typed:f64" THEN - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - MUTABLE convertedF: Float64[]@list = List[]; - PARTIAL MATCH listVal START - Value.List AS srcItems -> - FOR ci IN (0_i64 ..< srcItems.length()) DO - convertedF.append(getNum(srcItems[ci])); - END, - Value.TypedF64Arr -> RETURN listVal;, - Value.TypedI64Arr AS iarr -> - FOR ci IN (0_i64 ..< iarr.length()) DO - convertedF.append(toFloat(iarr[ci])); - END, - DEFAULT -> PASS; - END - RETURN Value{ TypedF64Arr: convertedF }; - - # Type conversion ops - ELSE_IF formName == "int->float" THEN - val = eval!(COPY items[1], envId, pool); - RETURN Value{ Number: toFloat(getInt(val)) }; - - ELSE_IF formName == "float->int" THEN - val = eval!(COPY items[1], envId, pool); - truncated = toInt(getNum(val)); - RETURN Value{ Int64Val: truncated }; - - ELSE_IF formName == "to-typed:i64" THEN - # (to-typed:i64 untypedList) -> convert Value[] list to Int64[] - listVal = eval!(COPY items[1], envId, pool); - IF isError?(listVal) THEN RETURN listVal; END - MUTABLE converted: Int64[]@list = List[]; - PARTIAL MATCH listVal START - Value.List AS srcItems -> - FOR ci IN (0_i64 ..< srcItems.length()) DO - converted.append(getInt(srcItems[ci])); - END, - Value.TypedI64Arr -> RETURN listVal;, - DEFAULT -> PASS; - END - RETURN Value{ TypedI64Arr: converted }; - - ELSE_IF formName == "to-list" THEN - # (to-list typedArr) -> convert TypedI64Arr back to Value[] list - arrVal = eval!(COPY items[1], envId, pool); - IF isError?(arrVal) THEN RETURN arrVal; END - PARTIAL MATCH arrVal START - Value.TypedI64Arr AS iarr -> - MUTABLE untyped: Value[]@list = List[]; - FOR ui IN (0_i64 ..< iarr.length()) DO - untyped.append(Value{ Int64Val: iarr[ui] }); - END - RETURN Value{ List: untyped };, - Value.List -> RETURN arrVal;, - DEFAULT -> RETURN arrVal; - END - RETURN arrVal; - - ELSE_IF formName == "typed-push:i64" THEN - # (typed-push:i64 arr val) -> new typed array with value appended - arrVal = eval!(COPY items[1], envId, pool); - IF isError?(arrVal) THEN RETURN arrVal; END - newVal = eval!(COPY items[2], envId, pool); - IF isError?(newVal) THEN RETURN newVal; END - MUTABLE newArr: Int64[]@list = List[]; - PARTIAL MATCH arrVal START - Value.TypedI64Arr AS iarr -> - FOR pi IN (0_i64 ..< iarr.length()) DO - newArr.append(iarr[pi]); - END, - DEFAULT -> PASS; - END - newArr.append(getInt(newVal)); - RETURN Value{ TypedI64Arr: newArr }; - - # FFI bridge: call native CLEAR functions with typed arrays - ELSE_IF formName == "native-sum" THEN - arrVal = eval!(COPY items[1], envId, pool); - IF isError?(arrVal) THEN RETURN arrVal; END - PARTIAL MATCH arrVal START - Value.TypedI64Arr AS iarr -> - result = nativeSum(iarr); - RETURN Value{ Int64Val: result };, - DEFAULT -> RETURN Value.Error{ errMsg: "native-sum requires typed Int64 array", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - ELSE_IF formName == "native-sum-f64" THEN - arrVal = eval!(COPY items[1], envId, pool); - IF isError?(arrVal) THEN RETURN arrVal; END - PARTIAL MATCH arrVal START - Value.TypedF64Arr AS farr -> - RETURN Value{ Number: nativeSumF64(farr) };, - DEFAULT -> RETURN Value.Error{ errMsg: "native-sum-f64 requires typed Float64 array", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - ELSE_IF formName == "native-dot" THEN - aVal = eval!(COPY items[1], envId, pool); - IF isError?(aVal) THEN RETURN aVal; END - bVal = eval!(COPY items[2], envId, pool); - IF isError?(bVal) THEN RETURN bVal; END - PARTIAL MATCH aVal START - Value.TypedF64Arr AS fa -> - PARTIAL MATCH bVal START - Value.TypedF64Arr AS fb -> - RETURN Value{ Number: nativeDot(fa, fb) };, - DEFAULT -> RETURN Value.Error{ errMsg: "native-dot requires two Float64 arrays", errKind: "Type", errType: "" }; - END, - DEFAULT -> RETURN Value.Error{ errMsg: "native-dot requires Float64 arrays", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - # Sandboxed I/O: check __sandbox flag before file/shell ops - ELSE_IF formName == "sandboxed-read" THEN - sandbox = envGet!(envId, "__sandbox", pool); - IF isTruthy?(sandbox) THEN - RETURN Value.Error{ errMsg: "I/O not permitted (run with --allow-io)", errKind: "Permission", errType: "" }; - END - pathVal = eval!(COPY items[1], envId, pool); - IF isError?(pathVal) THEN RETURN pathVal; END - content = readFile(getStr(pathVal)) OR RAISE; - RETURN Value{ Str: COPY content }; - - ELSE_IF formName == "sandboxed-write" THEN - sandbox = envGet!(envId, "__sandbox", pool); - IF isTruthy?(sandbox) THEN - RETURN Value.Error{ errMsg: "I/O not permitted (run with --allow-io)", errKind: "Permission", errType: "" }; - END - pathVal = eval!(COPY items[1], envId, pool); - IF isError?(pathVal) THEN RETURN pathVal; END - contentVal = eval!(COPY items[2], envId, pool); - IF isError?(contentVal) THEN RETURN contentVal; END - writeFile(getStr(pathVal), getStr(contentVal)); - RETURN Value.Nil; - - ELSE_IF formName == "sandboxed-shell" THEN - sandbox = envGet!(envId, "__sandbox", pool); - IF isTruthy?(sandbox) THEN - RETURN Value.Error{ errMsg: "shell not permitted (run with --allow-io)", errKind: "Permission", errType: "" }; - END - cmdVal = eval!(COPY items[1], envId, pool); - IF isError?(cmdVal) THEN RETURN cmdVal; END - output = shell(getStr(cmdVal)); - RETURN Value{ Str: COPY output }; - - ELSE_IF formName == "source-line" THEN - # (source-line N) -> track current CLEAR source line for error messages - lineVal = eval!(COPY items[1], envId, pool); - IF pool[envId] AS env THEN env.vars["__source_line"] = lineVal; END - RETURN Value.Nil; - - ELSE_IF formName == "sandbox-enable" THEN - IF pool[envId] AS env THEN env.vars["__sandbox"] = Value.TrueVal; END - RETURN Value.Nil; - - ELSE_IF formName == "native-manhattan" THEN - # (native-manhattan structA structB) -> Int64 manhattan distance - aVal = eval!(COPY items[1], envId, pool); - IF isError?(aVal) THEN RETURN aVal; END - bVal = eval!(COPY items[2], envId, pool); - IF isError?(bVal) THEN RETURN bVal; END - aCdr = pairCdr(aVal); - bCdr = pairCdr(bVal); - PARTIAL MATCH aCdr START - Value.TypedI64Arr AS ai -> - PARTIAL MATCH bCdr START - Value.TypedI64Arr AS bi -> - RETURN Value{ Int64Val: nativePointManhattan(ai, bi) };, - DEFAULT -> RETURN Value.Error{ errMsg: "expected TypedStructI64", errKind: "Type", errType: "" }; - END, - DEFAULT -> RETURN Value.Error{ errMsg: "expected TypedStructI64", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - ELSE_IF formName == "native-translate" THEN - # (native-translate struct dx dy) -> new TypedStructI64 - sVal = eval!(COPY items[1], envId, pool); - IF isError?(sVal) THEN RETURN sVal; END - dxVal = eval!(COPY items[2], envId, pool); - IF isError?(dxVal) THEN RETURN dxVal; END - dyVal = eval!(COPY items[3], envId, pool); - IF isError?(dyVal) THEN RETURN dyVal; END - tagVal = pairCar(sVal); - sCdr = pairCdr(sVal); - PARTIAL MATCH sCdr START - Value.TypedI64Arr AS si -> - MUTABLE trData: Int64[]@list = List[]; - trData.append(si[0] + getInt(dxVal)); - trData.append(si[1] + getInt(dyVal)); - RETURN Value.Pair{ pairCar: COPY tagVal, pairCdr: Value{ TypedI64Arr: trData } };, - DEFAULT -> RETURN Value.Error{ errMsg: "expected TypedStructI64", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - ELSE_IF formName == "native-contains" THEN - arrVal = eval!(COPY items[1], envId, pool); - IF isError?(arrVal) THEN RETURN arrVal; END - needleVal = eval!(COPY items[2], envId, pool); - IF isError?(needleVal) THEN RETURN needleVal; END - PARTIAL MATCH arrVal START - Value.TypedI64Arr AS iarr -> - RETURN boolVal(nativeContains(iarr, getInt(needleVal)));, - DEFAULT -> RETURN Value.Error{ errMsg: "native-contains requires typed Int64 array", errKind: "Type", errType: "" }; - END - RETURN Value.Nil; - - ELSE_IF formName == "debug-set-break" THEN - # (debug-set-break "fnName") -> register breakpoint - bpName = eval!(COPY items[1], envId, pool); - IF pool[envId] AS env THEN env.vars["__bp_" + getStr(bpName)] = Value.TrueVal; END - RETURN Value.Nil; - - ELSE_IF formName == "debug-clear-break" THEN - bpName = eval!(COPY items[1], envId, pool); - IF pool[envId] AS env THEN env.vars["__bp_" + getStr(bpName)] = Value.Nil; END - RETURN Value.Nil; - - # Error introspection: special forms to avoid error propagation - ELSE_IF formName == "error?" THEN - val = eval!(COPY items[1], envId, pool); - RETURN boolVal(isError?(val)); - - ELSE_IF formName == "error-message" THEN - val = eval!(COPY items[1], envId, pool); - RETURN Value{ Str: getErrMsg(val) }; - - ELSE_IF formName == "error-kind" THEN - val = eval!(COPY items[1], envId, pool); - RETURN Value{ Str: getErrKind(val) }; - - ELSE_IF formName == "quote" THEN - RETURN COPY items[1]; - - ELSE_IF formName == "raise" THEN - msg = eval!(COPY items[1], envId, pool); - IF isError?(msg) THEN RETURN msg; END - kind = eval!(COPY items[2], envId, pool); - IF isError?(kind) THEN RETURN kind; END - # Include source line in error message if tracked - srcLine = envGet!(envId, "__source_line", pool); - MUTABLE errMsg: String = getStr(msg); - IF getNum(srcLine) > 0.0 THEN - lineNum = toInt(getNum(srcLine)); - errMsg = errMsg + " (line " + lineNum.toString() + ")"; - END - RETURN Value.Error{ errMsg: COPY errMsg, errKind: COPY getStr(kind), errType: "" }; - - ELSE_IF formName == "try" THEN - # (try expr (catch e handler)) - MUTABLE tryResult: Value = eval!(COPY items[1], envId, pool); - PARTIAL MATCH tryResult START - Value.Error AS e -> - RETURN handleCatch!(items[2], e.errMsg, e.errKind, envId, pool);, - DEFAULT -> RETURN tryResult; - END - RETURN tryResult; - - ELSE_IF formName == "def!" || formName == "define" THEN - defName = getSymName(items[1]); - val = eval!(COPY items[2], envId, pool); - IF isError?(val) THEN RETURN val; END - IF pool[envId] AS env THEN env.vars[defName] = COPY val; END - RETURN val; - - ELSE_IF formName == "set!" THEN - setName = getSymName(items[1]); - setVal = eval!(COPY items[2], envId, pool); - IF isError?(setVal) THEN RETURN setVal; END - envSet!(envId, setName, setVal, pool); - RETURN setVal; - - ELSE_IF formName == "vector-set!" THEN - # (vector-set! var idx val) - copy-modify-store: get vector, rebuild with new slot, store back - vecName = getSymName(items[1]); - idxVal = eval!(COPY items[2], envId, pool); - IF isError?(idxVal) THEN RETURN idxVal; END - newElem = eval!(COPY items[3], envId, pool); - IF isError?(newElem) THEN RETURN newElem; END - idx = getInt(idxVal); - MUTABLE existingVec = envGet!(envId, vecName, pool); - MUTABLE newVec: Value[]@list = List[]; - PARTIAL MATCH existingVec START - Value.Vector AS oldVec -> - FOR vi IN (0_i64 ..< oldVec.length()) DO - IF vi == idx THEN - newVec.append(COPY newElem); - ELSE - newVec.append(COPY oldVec[vi]); - END - END - envSet!(envId, vecName, Value{ Vector: newVec }, pool);, - DEFAULT -> PASS; - END - RETURN Value.Nil; - - ELSE_IF formName == "list-remove-at!" THEN - # list-remove-at! varname idx: removes element at idx, returns removed element - lraName = getSymName(items[1]); - lraIdx = getInt(eval!(COPY items[2], envId, pool)); - lraCurrent = envGet!(envId, lraName, pool); - PARTIAL MATCH lraCurrent START - Value.List AS lraElems -> - IF lraIdx < 0 || lraIdx >= lraElems.length() THEN RETURN Value.Nil; END - lraRemoved = COPY lraElems[lraIdx]; - MUTABLE lraNew: Value[]@list = List[]; - FOR lri IN (0_i64 ..< lraElems.length()) DO - IF lri != lraIdx THEN lraNew.append(COPY lraElems[lri]); END - END - envSet!(envId, lraName, Value{ List: lraNew }, pool); - RETURN lraRemoved;, - Value.TypedI64Arr AS lraIarr -> - IF lraIdx < 0 || lraIdx >= lraIarr.length() THEN RETURN Value.Nil; END - lraRemovedI = lraIarr[lraIdx]; - MUTABLE lraNewI: Int64[]@list = List[]; - FOR lri IN (0_i64 ..< lraIarr.length()) DO - IF lri != lraIdx THEN lraNewI.append(lraIarr[lri]); END - END - envSet!(envId, lraName, Value{ TypedI64Arr: lraNewI }, pool); - RETURN Value{ Int64Val: lraRemovedI };, - Value.TypedF64Arr AS lraFarr -> - IF lraIdx < 0 || lraIdx >= lraFarr.length() THEN RETURN Value.Nil; END - lraRemovedF = lraFarr[lraIdx]; - MUTABLE lraNewF: Float64[]@list = List[]; - FOR lri IN (0_i64 ..< lraFarr.length()) DO - IF lri != lraIdx THEN lraNewF.append(lraFarr[lri]); END - END - envSet!(envId, lraName, Value{ TypedF64Arr: lraNewF }, pool); - RETURN Value{ Number: lraRemovedF };, - DEFAULT -> RETURN Value.Nil; - END - - ELSE_IF formName == "stream-next!" THEN - # (stream-next! var) - advance stream: return head, update var to tail - snName = getSymName(items[1]); - snCurrent = envGet!(envId, snName, pool); - PARTIAL MATCH snCurrent START - Value.List AS snElems -> - IF snElems.length() == 0 THEN RETURN Value.Nil; END - snHead = COPY snElems[0]; - MUTABLE snTail: Value[]@list = List[]; - FOR sni IN (1_i64 ..< snElems.length()) DO - snTail.append(COPY snElems[sni]); - END - envSet!(envId, snName, Value{ List: snTail }, pool); - RETURN snHead;, - DEFAULT -> RETURN Value.Nil; - END - - ELSE_IF formName == "let*" || formName == "let" THEN - PARTIAL MATCH items[1] START - Value.List AS binds -> - letId: Id = pool.insert(Env{ vars: {} }); - IF pool[letId] AS letEnv THEN - letEnv.vars["__p"] = Value{ EnvRef: envId }; - IF binds.length() > 0 && isList?(binds[0]) THEN - FOR bi IN (0_i64 ..< binds.length()) DO - PARTIAL MATCH binds[bi] START - Value.List AS pair -> - bName = getSymName(pair[0]); - bVal = eval!(COPY pair[1], letId, pool); - IF isError?(bVal) THEN RETURN bVal; END - letEnv.vars[bName] = bVal;, - DEFAULT -> PASS; - END - END - ELSE - MUTABLE bi: Int64 = 0; - WHILE bi < binds.length() DO - bName = getSymName(binds[bi]); - bVal = eval!(COPY binds[bi + 1], letId, pool); - IF isError?(bVal) THEN RETURN bVal; END - letEnv.vars[bName] = bVal; - bi += 2; - END - END - END - RETURN Value.Tco{ tcoAst: COPY items[2], tcoEnv: letId };, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; - - ELSE_IF formName == "fn*" || formName == "lambda" THEN - PARTIAL MATCH items[1] START - Value.List AS pnames -> - MUTABLE lambdaBody: Value = Value.Nil; - IF items.length() > 2 THEN lambdaBody = COPY items[2]; END - RETURN Value.Lambda{ params: COPY pnames, body: lambdaBody, envId: envId };, - DEFAULT -> RETURN Value.Nil; - END - RETURN Value.Nil; - - ELSE_IF formName == "do" || formName == "begin" THEN - FOR di IN (1_i64 ..< items.length() - 1) DO - stepResult = eval!(COPY items[di], envId, pool); - IF isError?(stepResult) THEN RETURN stepResult; END - END - RETURN Value.Tco{ tcoAst: COPY items[items.length() - 1], tcoEnv: envId }; - - ELSE_IF formName == "while" THEN - MUTABLE whileCond: Value = eval!(COPY items[1], envId, pool); - WHILE isTruthy?(whileCond) DO - FOR wi IN (2_i64 ..< items.length()) DO - whileStep = eval!(COPY items[wi], envId, pool); - IF isError?(whileStep) THEN RETURN whileStep; END - END - whileCond = eval!(COPY items[1], envId, pool); - END - RETURN Value.Nil; - - ELSE_IF formName == "if" THEN - cond = eval!(COPY items[1], envId, pool); - IF isError?(cond) THEN RETURN cond; END - IF isTruthy?(cond) THEN - RETURN Value.Tco{ tcoAst: COPY items[2], tcoEnv: envId }; - ELSE - IF items.length() > 3 THEN - RETURN Value.Tco{ tcoAst: COPY items[3], tcoEnv: envId }; - END - RETURN Value.Nil; - END - - ELSE - MUTABLE evaled: Value[]@list = List[]; - FOR ei IN (0_i64 ..< items.length()) DO - argVal = eval!(COPY items[ei], envId, pool); - IF isError?(argVal) THEN RETURN argVal; END - evaled.append(COPY argVal); - END - f = evaled[0]; - - # Debug: check breakpoints + step mode - calledName = getSymName(items[0]); - IF calledName.length() > 0 THEN - # Build call description - MUTABLE callDesc = calledName + "("; - FOR ai IN (1_i64 ..< evaled.length()) DO - IF ai > 1 THEN callDesc = callDesc + ", "; END - callDesc = callDesc + prStr(evaled[ai], TRUE); - END - callDesc = callDesc + ")"; - - # Check if we should break: explicit breakpoint OR step mode - bpKey = envGet!(envId, "__bp_" + calledName, pool); - stepMode = getNum(envGet!(envId, "__dbg_step", pool)); - curDepth = getNum(envGet!(envId, "__dbg_depth", pool)); - targetDepth = getNum(envGet!(envId, "__dbg_target_depth", pool)); - MUTABLE shouldBreak = isTruthy?(bpKey); - - # Step-into: always break - IF stepMode == 1.0 THEN shouldBreak = TRUE; END - # Step-over: break when depth <= target - IF stepMode == 2.0 && curDepth <= targetDepth THEN shouldBreak = TRUE; END - # Step-out: break when depth < target - IF stepMode == 3.0 && curDepth < targetDepth THEN shouldBreak = TRUE; END - - IF shouldBreak THEN - # Push call stack - oldStack = getStr(envGet!(envId, "__dbg_stack", pool)); - IF pool[envId] AS dbgEnv THEN - IF oldStack.length() > 0 THEN - dbgEnv.vars["__dbg_stack"] = Value{ Str: COPY callDesc + " < " + oldStack }; - ELSE - dbgEnv.vars["__dbg_stack"] = Value{ Str: COPY callDesc }; - END - END - - MUTABLE action = debugPause!(callDesc, envId, "", pool); - - # Handle inspect requests from debugger - WHILE action == 4 DO - inspAst = envGet!(envId, "__dbg_inspect", pool); - inspResult = eval!(COPY inspAst, envId, pool); - action = debugPause!(callDesc, envId, prStr(inspResult, TRUE), pool); - END - - # Set step mode based on debug action - IF pool[envId] AS dbgEnv2 THEN - dbgEnv2.vars["__dbg_step"] = Value{ Number: toFloat(action) }; - dbgEnv2.vars["__dbg_target_depth"] = Value{ Number: curDepth }; - # Restore stack - dbgEnv2.vars["__dbg_stack"] = Value{ Str: COPY oldStack }; - END - END - - # Track call depth for step-over/out - IF pool[envId] AS dbgEnv3 THEN dbgEnv3.vars["__dbg_depth"] = Value{ Number: curDepth + 1.0 }; END - END - - IF isLambda?(f) THEN - PARTIAL MATCH f START - Value.Lambda AS lam -> - callId: Id = pool.insert(Env{ vars: {} }); - IF pool[callId] AS callEnv THEN - callEnv.vars["__p"] = Value{ EnvRef: lam.envId }; - FOR pi IN (0_i64 ..< lam.params.length()) DO - pname = getSymName(lam.params[pi]); - callEnv.vars[pname] = evaled[pi + 1]; - END - END - bodyAst: Value = COPY lam.body; - RETURN eval!(GIVE bodyAst, callId, pool);, - DEFAULT -> RETURN Value.Nil; - END - ELSE - fnId = getNativeId(f); - IF fnId > 0 THEN - RETURN applyNative(fnId, evaled); - END - RETURN Value.Nil; - END - END -END - -# runTest: tokenize + parse + eval - -FN runTest!(input: String, envId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS Value EFFECTS REENTRANT -> - tokenizeToEnv!(penv, input); - penv["__rp"] = Value{ Number: 0.0 }; - ast = readFormEnv!(penv); - RETURN eval!(COPY ast, envId, pool); -END - -# Setup: create root env with all native functions registered. -# Returns the root env Id. - -FN setupEnv!(MUTABLE pool: Env[50000]@pool) RETURNS Id -> - rootId: Id = pool.insert(Env{ vars: {} }); - IF pool[rootId] AS root THEN - # Arithmetic: 1-4 - root.vars["+"] = Value{ NativeFn: 1 }; - root.vars["-"] = Value{ NativeFn: 2 }; - root.vars["*"] = Value{ NativeFn: 3 }; - root.vars["/"] = Value{ NativeFn: 4 }; - # Comparison: 5-9 - root.vars["="] = Value{ NativeFn: 5 }; - root.vars["<"] = Value{ NativeFn: 6 }; - root.vars[">"] = Value{ NativeFn: 7 }; - root.vars["<="] = Value{ NativeFn: 8 }; - root.vars[">="] = Value{ NativeFn: 9 }; - # List: 10-15 - root.vars["list"] = Value{ NativeFn: 10 }; - root.vars["list?"] = Value{ NativeFn: 11 }; - root.vars["empty?"] = Value{ NativeFn: 12 }; - root.vars["count"] = Value{ NativeFn: 13 }; - root.vars["not"] = Value{ NativeFn: 14 }; - root.vars["prn"] = Value{ NativeFn: 15 }; - # Vector: 16-20 - root.vars["vector"] = Value{ NativeFn: 16 }; - root.vars["vector-ref"] = Value{ NativeFn: 17 }; - root.vars["vector-set!"] = Value{ NativeFn: 18 }; - root.vars["vector-length"] = Value{ NativeFn: 19 }; - root.vars["vector?"] = Value{ NativeFn: 20 }; - # Pair: 21-24 - root.vars["cons"] = Value{ NativeFn: 21 }; - root.vars["car"] = Value{ NativeFn: 22 }; - root.vars["cdr"] = Value{ NativeFn: 23 }; - root.vars["pair?"] = Value{ NativeFn: 24 }; - # Symbol comparison: 25 - root.vars["eq?"] = Value{ NativeFn: 25 }; - # String: 26-33 - root.vars["string-append"] = Value{ NativeFn: 26 }; - root.vars["string-length"] = Value{ NativeFn: 27 }; - root.vars["substring"] = Value{ NativeFn: 28 }; - root.vars["string-ref"] = Value{ NativeFn: 29 }; - root.vars["number->string"] = Value{ NativeFn: 30 }; - root.vars["string->number"] = Value{ NativeFn: 31 }; - root.vars["string?"] = Value{ NativeFn: 32 }; - root.vars["display"] = Value{ NativeFn: 33 }; - # Modulo: 37 - root.vars["modulo"] = Value{ NativeFn: 37 }; - # List access: 34=list-ref, 35=list-length, 36=list-push, 62=list-set! - root.vars["list-ref"] = Value{ NativeFn: 34 }; - root.vars["list-length"] = Value{ NativeFn: 35 }; - root.vars["length"] = Value{ NativeFn: 35 }; - root.vars["list-push"] = Value{ NativeFn: 36 }; - root.vars["list-set!"] = Value{ NativeFn: 62 }; - # String stdlib: 38-42 + aliases - root.vars["startsWith?"] = Value{ NativeFn: 38 }; - root.vars["split"] = Value{ NativeFn: 39 }; - root.vars["indexOf"] = Value{ NativeFn: 40 }; - root.vars["contains?"] = Value{ NativeFn: 41 }; - root.vars["trim"] = Value{ NativeFn: 42 }; - root.vars["charAt"] = Value{ NativeFn: 29 }; - root.vars["substr"] = Value{ NativeFn: 43 }; - root.vars["toNumber"] = Value{ NativeFn: 31 }; - root.vars["toInt"] = Value{ NativeFn: 44 }; - root.vars["toFloat"] = Value{ NativeFn: 30 }; - root.vars["endsWith?"] = Value{ NativeFn: 48 }; - root.vars["join"] = Value{ NativeFn: 49 }; - # Math: 50-56 - root.vars["abs"] = Value{ NativeFn: 50 }; - root.vars["min"] = Value{ NativeFn: 51 }; - root.vars["max"] = Value{ NativeFn: 52 }; - root.vars["floor"] = Value{ NativeFn: 53 }; - root.vars["timestampMs"] = Value{ NativeFn: 54 }; - root.vars["random"] = Value{ NativeFn: 55 }; - root.vars["randomInt"] = Value{ NativeFn: 56 }; - # File I/O: 45-47 - root.vars["readFile"] = Value{ NativeFn: 45 }; - root.vars["writeFile"] = Value{ NativeFn: 46 }; - root.vars["shell"] = Value{ NativeFn: 47 }; - # String methods: 57-61 - root.vars["codepointCount"] = Value{ NativeFn: 57 }; - root.vars["bytes"] = Value{ NativeFn: 58 }; - root.vars["replace"] = Value{ NativeFn: 59 }; - root.vars["uppercase"] = Value{ NativeFn: 60 }; - root.vars["lowercase"] = Value{ NativeFn: 61 }; - root.vars["set-insert"] = Value{ NativeFn: 63 }; - root.vars["set-remove"] = Value{ NativeFn: 64 }; - root.vars["parse-i64"] = Value{ NativeFn: 65 }; - END - # error?, error-message, error-kind, list-append! are special forms - RETURN rootId; -END - -# ============================================================================ -# Bytecode VM -# ============================================================================ - -# Opcodes: each instruction is an Int64 in the ops array. -# Operands follow the opcode inline in ops. -# -# Opcode table: -# 1 loadConst [idx] push consts[idx] -# 2 loadName [idx] push env lookup of consts[idx] (symbol string) -# 3 storeName [idx] pop value, bind consts[idx] in current env -# 4 pop discard top of stack -# 5 dup duplicate top of stack -# 10 add pop b, pop a, push a+b -# 11 sub pop b, pop a, push a-b -# 12 mul pop b, pop a, push a*b -# 13 div pop b, pop a, push a/b -# 14 neg pop a, push -a -# 20 eq pop b, pop a, push a==b -# 21 lt pop b, pop a, push ab -# 23 lte pop b, pop a, push a<=b -# 24 gte pop b, pop a, push a>=b -# 30 not pop a, push !a -# 40 jump [offset] unconditional jump to offset -# 41 jumpIfFalse [offset] pop, jump if falsy -# 42 call [argc] call function on stack with argc args -# 43 tailCall [argc] tail call (reuse frame) -# 44 ret return top of stack to caller -# 50 makeList [count] pop count items, push list -# 51 makeVec [count] pop count items, push vector -# 52 cons pop cdr, pop car, push pair -# 53 car pop pair, push car -# 54 cdr pop pair, push cdr -# 60 makeClosure [idx] push closure capturing consts[idx] (sub-Chunk index) -# 61 setName [idx] pop value, set! consts[idx] in env chain -# 70 nativeCall [id, argc] call native fn by id with argc args -# 71 halt stop execution, top of stack is result -# 80 loadSlot [idx] push slots[idx] (Commit 10) -# 81 storeSlot [idx] pop, store in slots[idx] (Commit 10) - -# A compiled bytecode chunk: flat instruction array + constant pool + source lines. - -STRUCT Chunk { - ops: Int64[]@list, - consts: Value[]@list, - lines: Int64[]@list -} - -# A call frame tracks the return point and base pointer for each function call. - -STRUCT Frame { - chunkIdx: Int64, - returnIp: Int64, - baseSp: Int64, - envId: Id -} - -# Bytecode loader: reads ops and consts from files written by Ruby compiler - -FN loadBytecodeOps!(path: String, MUTABLE pool: Env[50000]@pool) RETURNS Int64[] -> - raw = readFile(path) OR RAISE; - # Split on commas using the interpreter's split function - parts = split(raw, ","); - MUTABLE ops: Int64[]@list = List[]; - FOR pi IN (0_i64 ..< parts.length()) DO - part = trim(parts[pi]); - IF part.length() > 0 THEN - n = toNumber(part) OR 0.0; - intN = toInt(n); - ops.append(intN); - END - END - RETURN ops; -END - -FN loadBytecodeConsts!(path: String, MUTABLE pool: Env[50000]@pool) RETURNS Value[] -> - raw = readFile(path) OR RAISE; - MUTABLE consts: Value[]@list = List[]; - # Parse line by line - MUTABLE lineStart: Int64 = 0; - FOR ci IN (0_i64 ..< raw.length()) DO - IF charAt(raw, ci) == "\n" THEN - line = substr(raw, lineStart, ci - lineStart); - consts.append(parseConstLine!(line, pool)); - lineStart = ci + 1; - END - END - # Last line (no trailing newline) - IF lineStart < raw.length() THEN - line = substr(raw, lineStart, raw.length() - lineStart); - consts.append(parseConstLine!(line, pool)); - END - RETURN consts; -END - -FN parseConstLine!(line: String, MUTABLE pool: Env[50000]@pool) RETURNS Value -> - IF line == "N" THEN RETURN Value.Nil; END - IF startsWith?(line, "I:") THEN - numStr = substr(line, 2, line.length() - 2); - n = toNumber(numStr) OR 0.0; - intVal = toInt(n); - RETURN Value{ Int64Val: intVal }; - END - IF startsWith?(line, "F:") THEN - numStr = substr(line, 2, line.length() - 2); - n = toNumber(numStr) OR 0.0; - RETURN Value{ Number: n }; - END - IF startsWith?(line, "S:") THEN - RETURN Value{ Str: substr(line, 2, line.length() - 2) }; - END - IF startsWith?(line, "B:") THEN - IF substr(line, 2, line.length() - 2) == "true" THEN RETURN Value.TrueVal; END - RETURN Value.FalseVal; - END - IF startsWith?(line, "SYM:") THEN - RETURN Value{ Symbol: substr(line, 4, line.length() - 4) }; - END - IF line == "L" THEN - MUTABLE empty: Value[]@list = List[]; - RETURN Value{ List: empty }; - END - RETURN Value.Nil; -END - -# Chunk builder: emit helpers cannot take @list params (transpiler bug extracts .items). -# Build bytecode by appending directly to ops/consts/lines arrays inline. - -# Bytecode dispatch loop. Executes ops using an operand stack. -# Returns the value left on top of the stack at halt. - -# ============================================================================ -# Bytecode Compiler: AST (Value) -> ops/consts arrays -# ============================================================================ -# compile! walks a parsed S-expression and returns a Value.Pair where: -# car = Value.List of Value.Number (the ops, encoded as floats) -# cdr = Value.List of Value (the constant pool) -# The caller extracts these into Int64[]@list and Value[]@list for exec!. - -# compile!: compiles a single S-expression into bytecode. -# Returns Pair{List[ops as Numbers], List[consts]}. -# Sub-expressions are compiled as loadConst (literals/results of eval!) -# or loadName (symbols). @list can't be passed between functions, so -# the compiler handles each form in one flat function. -# The real compiler will be in Ruby (scheme_transpiler.rb). - -# compileArg pattern (inlined - can't pass @list to functions): -# Symbol -> loadName, List -> eval! + loadConst, Literal -> loadConst - -# compile! stores results in pool env at __bc_ops and __bc_consts keys. -# Caller reads them from pool[envId] after the call. - -FN compile!(ast: Value, envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Void EFFECTS REENTRANT -> - MUTABLE ops: Value[]@list = List[]; - MUTABLE consts: Value[]@list = List[]; - - IF isSymbol?(ast) THEN - symName = getSymName(ast); - # Check if this symbol has a slot assignment - MUTABLE slotLookup = Value{ Number: 0.0 - 1.0 }; - IF pool[envId] AS slotEnv THEN slotLookup = slotEnv.vars["__slot_" + symName] OR Value{ Number: 0.0 - 1.0 }; END - slotNum = toInt(getNum(slotLookup)); - IF slotNum >= 0 THEN - # LOAD_SLOT - ops.append(Value{ Number: 20.0 }); ops.append(Value{ Number: toFloat(slotNum) }); - ELSE - # LOAD_NAME - cidx = consts.length(); consts.append(COPY ast); - ops.append(Value{ Number: 1.0 }); ops.append(Value{ Number: toFloat(cidx) }); - END - ELSE_IF isList?(ast) == FALSE THEN - cidx = consts.length(); consts.append(COPY ast); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(cidx) }); - ELSE - PARTIAL MATCH ast START Value.List AS items -> - IF items.length() > 0 THEN - formName = getSymName(items[0]); - - IF formName == "quote" THEN - cidx = consts.length(); consts.append(COPY items[1]); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(cidx) }); - - ELSE_IF formName == "+" || formName == "-" || formName == "*" || formName == "/" || formName == "=" || formName == "<" || formName == ">" || formName == "<=" || formName == ">=" THEN - ev1 = eval!(COPY items[1], envId, pool); - c1 = consts.length(); consts.append(ev1); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c1) }); - ev2 = eval!(COPY items[2], envId, pool); - c2 = consts.length(); consts.append(ev2); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c2) }); - IF formName == "+" THEN ops.append(Value{ Number: 4.0 }); - ELSE_IF formName == "-" THEN ops.append(Value{ Number: 5.0 }); - ELSE_IF formName == "*" THEN ops.append(Value{ Number: 6.0 }); - ELSE_IF formName == "/" THEN ops.append(Value{ Number: 7.0 }); - ELSE_IF formName == "=" THEN ops.append(Value{ Number: 8.0 }); - ELSE_IF formName == "<" THEN ops.append(Value{ Number: 9.0 }); - ELSE_IF formName == ">" THEN ops.append(Value{ Number: 10.0 }); - ELSE_IF formName == "<=" THEN ops.append(Value{ Number: 11.0 }); - ELSE_IF formName == ">=" THEN ops.append(Value{ Number: 12.0 }); - END - - ELSE_IF formName == "not" THEN - ev1 = eval!(COPY items[1], envId, pool); - c1 = consts.length(); consts.append(ev1); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c1) }); - ops.append(Value{ Number: 13.0 }); - - ELSE_IF formName == "define" || formName == "def!" THEN - ev1 = eval!(COPY items[2], envId, pool); - c1 = consts.length(); consts.append(ev1); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c1) }); - defName = getSymName(items[1]); - MUTABLE nextSlot: Int64 = 0; - IF pool[envId] AS defSlotEnv THEN - slotCounter = defSlotEnv.vars["__slotN"] OR Value{ Number: 0.0 }; - nextSlot = toInt(getNum(slotCounter)); - defSlotEnv.vars["__slot_" + defName] = Value{ Number: toFloat(nextSlot) }; - defSlotEnv.vars["__slotN"] = Value{ Number: toFloat(nextSlot + 1) }; - END - ops.append(Value{ Number: 21.0 }); ops.append(Value{ Number: toFloat(nextSlot) }); - cidx = consts.length(); consts.append(Value{ Symbol: COPY defName }); - ops.append(Value{ Number: 2.0 }); ops.append(Value{ Number: toFloat(cidx) }); - - ELSE_IF formName == "if" THEN - ev1 = eval!(COPY items[1], envId, pool); - c1 = consts.length(); consts.append(ev1); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c1) }); - ops.append(Value{ Number: 15.0 }); - jumpFalseIdx = ops.length(); ops.append(Value{ Number: 0.0 }); - ev2 = eval!(COPY items[2], envId, pool); - c2 = consts.length(); consts.append(ev2); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c2) }); - ops.append(Value{ Number: 14.0 }); - jumpEndIdx = ops.length(); ops.append(Value{ Number: 0.0 }); - ops[jumpFalseIdx] = Value{ Number: toFloat(ops.length()) }; - IF items.length() > 3 THEN - ev3 = eval!(COPY items[3], envId, pool); - c3 = consts.length(); consts.append(ev3); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(c3) }); - ELSE - cidx = consts.length(); consts.append(Value.Nil); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(cidx) }); - END - ops[jumpEndIdx] = Value{ Number: toFloat(ops.length()) }; - - ELSE_IF formName == "begin" || formName == "do" THEN - FOR di IN (1_i64 ..< items.length()) DO - evd = eval!(COPY items[di], envId, pool); - cd = consts.length(); consts.append(evd); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(cd) }); - IF di < items.length() - 1 THEN ops.append(Value{ Number: 3.0 }); END - END - - ELSE_IF formName == "debug" THEN - ops.append(Value{ Number: 58.0 }); - - ELSE - FOR ai IN (0_i64 ..< items.length()) DO - eva = eval!(COPY items[ai], envId, pool); - ca = consts.length(); consts.append(eva); - ops.append(Value{ Number: 0.0 }); ops.append(Value{ Number: toFloat(ca) }); - END - ops.append(Value{ Number: 16.0 }); - ops.append(Value{ Number: toFloat(items.length() - 1) }); - END - END, - DEFAULT -> PASS; - END - END - - ops.append(Value{ Number: 19.0 }); - # Store in env entry-by-entry (Value.Number is inline, survives arena free) - IF pool[envId] AS bcEnv THEN - bcEnv.vars["__bc_opN"] = Value{ Number: toFloat(ops.length()) }; - FOR wi IN (0_i64 ..< ops.length()) DO - bcEnv.vars["__bc_o" + wi.toString()] = ops[wi]; - END - bcEnv.vars["__bc_cN"] = Value{ Number: toFloat(consts.length()) }; - FOR wi IN (0_i64 ..< consts.length()) DO - bcEnv.vars["__bc_c" + wi.toString()] = consts[wi]; - END - END - RETURN; -END - -# ============================================================================ - -# Bytecode dispatch loop. Executes ops using a stack-pointer-based operand stack. -# Push: IF sp >= stack.length() THEN stack.append(val) ELSE stack[sp] = val END; sp += 1 -# Returns the value on top of the stack at halt. - -FN exec!(ops: Int64[], consts: Value[], envId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Value EFFECTS REENTRANT -> - MUTABLE stack: Value[]@list = List[]; - MUTABLE slots: Value[]@list = List[]; - MUTABLE sp: Int64 = 0; - MUTABLE ip: Int64 = 0; - MUTABLE curEnv: Id = COPY envId; - MUTABLE running = TRUE; - # Typed stacks: avoid 40-byte Value union copies for typed arithmetic - MUTABLE istack: Int64[] = []; - MUTABLE isp: Int64 = 0; - MUTABLE fstack: Float64[] = []; - MUTABLE fsp: Int64 = 0; - # Native typed slots: avoid Value wrapping for i64/f64 locals - MUTABLE islots: Int64[] = []; - MUTABLE fslots: Float64[] = []; - # Pre-allocate 64 slots and 64 typed stack/slot entries - FOR si IN (0_i64 ..< 64) DO slots.append(Value.Nil); END - FOR si IN (0_i64 ..< 64) DO istack.append(0_i64); END - FOR si IN (0_i64 ..< 64) DO fstack.append(0.0); END - FOR si IN (0_i64 ..< 64) DO islots.append(0_i64); END - FOR si IN (0_i64 ..< 64) DO fslots.append(0.0); END - - WHILE running && ip < ops.length() DO - MUTABLE pv: Value = Value.Nil; - op = ops[ip]; - ip += 1; - - PARTIAL MATCH op START - 0 -> # LOAD_CONST - idx = ops[ip]; ip += 1; - pv = COPY consts[idx]; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 1 -> # LOAD_NAME - idx = ops[ip]; ip += 1; - pv = envGet!(curEnv, getSymName(consts[idx]), pool); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 2 -> # STORE_NAME - idx = ops[ip]; ip += 1; - IF pool[curEnv] AS storeEnv THEN storeEnv.vars[getSymName(consts[idx])] = stack[sp - 1]; END, - 3 -> # POP - sp -= 1;, - 4 -> # ADD (polymorphic) - PARTIAL MATCH stack[sp - 2] START - Value.Str AS s1 -> - pv = Value{ Str: s1 + getStr(stack[sp - 1]) };, - Value.Int64Val AS ia -> - IF isInt64?(stack[sp - 1]) THEN pv = Value{ Int64Val: ia + getInt(stack[sp - 1]) }; - ELSE pv = Value{ Number: toFloat(ia) + getNum(stack[sp - 1]) }; END, - DEFAULT -> - pv = Value{ Number: getNum(stack[sp - 2]) + getNum(stack[sp - 1]) }; - END - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 5 -> # SUB - pv = Value{ Number: getNum(stack[sp - 2]) - getNum(stack[sp - 1]) }; - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 6 -> # MUL - pv = Value{ Number: getNum(stack[sp - 2]) * getNum(stack[sp - 1]) }; - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 7 -> # DIV - pv = Value{ Number: getNum(stack[sp - 2]) / getNum(stack[sp - 1]) }; - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 8 -> # EQ - pv = boolVal(valEqual?(stack[sp - 2], stack[sp - 1])); - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 9 -> # LT - pv = boolVal(getNum(stack[sp - 2]) < getNum(stack[sp - 1])); - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 10 -> # GT - pv = boolVal(getNum(stack[sp - 2]) > getNum(stack[sp - 1])); - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 11 -> # LTE - pv = boolVal(getNum(stack[sp - 2]) <= getNum(stack[sp - 1])); - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 12 -> # GTE - pv = boolVal(getNum(stack[sp - 2]) >= getNum(stack[sp - 1])); - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 13 -> # NOT - sp -= 1; - pv = boolVal(isTruthy?(stack[sp]) == FALSE); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 14 -> # JUMP - ip = ops[ip];, - 15 -> # JUMP_IF_FALSE - target = ops[ip]; ip += 1; - sp -= 1; - IF isTruthy?(stack[sp]) == FALSE THEN ip = target; END, - 16 -> # CALL [argc] - argc = ops[ip]; ip += 1; - fnVal = stack[sp - argc - 1]; - fnId = getNativeId(fnVal); - IF fnId > 0 THEN - MUTABLE cArgs: Value[]@list = List[]; - cArgs.append(Value.Nil); - FOR ci IN (0_i64 ..< argc) DO - cArgs.append(COPY stack[sp - argc + ci]); - END - sp -= argc + 1; - pv = applyNative(fnId, cArgs); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1; - ELSE_IF isLambda?(fnVal) THEN - PARTIAL MATCH fnVal START - Value.Lambda AS lam -> - callId: Id = pool.insert(Env{ vars: {} }); - IF pool[callId] AS bcCallEnv THEN - bcCallEnv.vars["__p"] = Value{ EnvRef: lam.envId }; - FOR pi IN (0_i64 ..< argc) DO - pname = getSymName(lam.params[pi]); - bcCallEnv.vars[pname] = COPY stack[sp - argc + pi]; - END - END - sp -= argc + 1; - pv = eval!(COPY lam.body, callId, pool); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - DEFAULT -> - sp -= argc + 1; - pv = Value.Nil; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1; - END - ELSE - sp -= argc + 1; - pv = Value.Nil; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1; - END, - 17 -> # SET_NAME - idx = ops[ip]; ip += 1; - sp -= 1; - setVal = COPY stack[sp]; - envSet!(curEnv, getSymName(consts[idx]), setVal, pool); - pv = COPY setVal; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 18 -> # NATIVE_CALL [nid] [argc] - nid = ops[ip]; ip += 1; - argc = ops[ip]; ip += 1; - MUTABLE nArgs: Value[]@list = List[]; - nArgs.append(Value.Nil); - FOR ni IN (0_i64 ..< argc) DO - nArgs.append(COPY stack[sp - argc + ni]); - END - sp -= argc; - pv = applyNative(nid, nArgs); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 19 -> # HALT - running = FALSE;, - 20 -> # LOAD_SLOT - slotIdx = ops[ip]; ip += 1; - pv = slots[slotIdx]; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 21 -> # STORE_SLOT - slotIdx = ops[ip]; ip += 1; - slots[slotIdx] = stack[sp - 1];, - 22 -> # ADD_I64 (istack) - istack[isp - 2] = istack[isp - 2] + istack[isp - 1]; - isp -= 1;, - 23 -> # SUB_I64 (istack) - istack[isp - 2] = istack[isp - 2] - istack[isp - 1]; - isp -= 1;, - 24 -> # MUL_I64 (istack) - istack[isp - 2] = istack[isp - 2] * istack[isp - 1]; - isp -= 1;, - 25 -> # LT_I64 (istack -> istack, 0/1) - IF istack[isp - 2] < istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 26 -> # EQ_I64 (istack -> istack, 0/1) - IF istack[isp - 2] == istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 27 -> # INT_TO_F64 (istack -> fstack) - isp -= 1; - convFloat = toFloat(istack[isp]); - fstack[fsp] = convFloat; - fsp += 1;, - 28 -> # F64_TO_INT (fstack -> istack) - fsp -= 1; - convInt = toInt(fstack[fsp]); - istack[isp] = convInt; - isp += 1;, - 29 -> # MOD_I64 (istack) - ia = istack[isp - 2]; - ib = istack[isp - 1]; - IF ib != 0 THEN istack[isp - 2] = ia MOD ib; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 30 -> # GTE_I64 (istack -> istack, 0/1) - IF istack[isp - 2] >= istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 31 -> # GT_I64 (istack -> istack, 0/1) - IF istack[isp - 2] > istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 32 -> # LTE_I64 (istack -> istack, 0/1) - IF istack[isp - 2] <= istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 33 -> # NEQ_I64 (istack -> istack, 0/1) - IF istack[isp - 2] != istack[isp - 1] THEN istack[isp - 2] = 1_i64; ELSE istack[isp - 2] = 0_i64; END - isp -= 1;, - 34 -> # DIV_I64 (istack) - ia = istack[isp - 2]; - ib = istack[isp - 1]; - MUTABLE divR: Int64 = 0; - IF ib != 0 THEN divR = toInt(toFloat(ia) / toFloat(ib)); END - istack[isp - 2] = divR; - isp -= 1;, - 35 -> # JUMP_BACK - ip = ops[ip];, - 36 -> # CONCAT - s1 = prStr(stack[sp - 2], FALSE); - s2 = prStr(stack[sp - 1], FALSE); - pv = Value{ Str: s1 + s2 }; - sp -= 2; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 37 -> # DEFINE_FN - sexprIdx = ops[ip]; ip += 1; - nameIdx = ops[ip]; ip += 1; - sexprStr = getStr(consts[sexprIdx]); - MUTABLE defPenv: HashMap = {}; - pv = runTest!(sexprStr, curEnv, pool, defPenv);, - 38 -> # LOAD_SLOT_I64 [slot] (slots -> istack) - slotIdx = ops[ip]; ip += 1; - istack[isp] = getInt(slots[slotIdx]); - isp += 1;, - 39 -> # STORE_SLOT_I64 [slot] (istack -> slots) - slotIdx = ops[ip]; ip += 1; - isp -= 1; - slots[slotIdx] = Value{ Int64Val: istack[isp] };, - 40 -> # LOAD_CONST_I64 [idx] (consts -> istack) - idx = ops[ip]; ip += 1; - istack[isp] = getInt(consts[idx]); - isp += 1;, - 41 -> # JUMP_IF_FALSE_I (istack, 0=false) - isp -= 1; - IF istack[isp] == 0 THEN ip = ops[ip]; ELSE ip += 1; END, - 42 -> # LOAD_SLOT_F64 [slot] (slots -> fstack) - slotIdx = ops[ip]; ip += 1; - fstack[fsp] = getNum(slots[slotIdx]); - fsp += 1;, - 43 -> # STORE_SLOT_F64 [slot] (fstack -> slots) - slotIdx = ops[ip]; ip += 1; - fsp -= 1; - slots[slotIdx] = Value{ Number: fstack[fsp] };, - 44 -> # LOAD_CONST_F64 [idx] (consts -> fstack) - idx = ops[ip]; ip += 1; - fstack[fsp] = getNum(consts[idx]); - fsp += 1;, - 45 -> # ADD_F64 (fstack) - fstack[fsp - 2] = fstack[fsp - 2] + fstack[fsp - 1]; - fsp -= 1;, - 46 -> # SUB_F64 (fstack) - fstack[fsp - 2] = fstack[fsp - 2] - fstack[fsp - 1]; - fsp -= 1;, - 47 -> # MUL_F64 (fstack) - fstack[fsp - 2] = fstack[fsp - 2] * fstack[fsp - 1]; - fsp -= 1;, - 48 -> # DIV_F64 (fstack) - IF fstack[fsp - 1] != 0.0 THEN fstack[fsp - 2] = fstack[fsp - 2] / fstack[fsp - 1]; ELSE fstack[fsp - 2] = 0.0; END - fsp -= 1;, - 49 -> # LT_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] < fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 50 -> # GT_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] > fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 51 -> # LTE_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] <= fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 52 -> # GTE_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] >= fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 53 -> # EQ_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] == fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 54 -> # NEQ_F64 (fstack -> istack, 0/1) - IF fstack[fsp - 2] != fstack[fsp - 1] THEN istack[isp] = 1_i64; ELSE istack[isp] = 0_i64; END - fsp -= 2; isp += 1;, - 55 -> # I_TO_VAL (istack -> Value stack as Int64Val) - isp -= 1; - pv = Value{ Int64Val: istack[isp] }; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 56 -> # F_TO_VAL (fstack -> Value stack) - fsp -= 1; - pv = Value{ Number: fstack[fsp] }; - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 57 -> # BOOL_TO_VAL (istack 0/1 -> Value stack TrueVal/FalseVal) - isp -= 1; - pv = boolVal(istack[isp] != 0); - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 58 -> # DEBUG_BREAK - print("--- debug break at ip=" + toString(ip) + " ---"); - MUTABLE dbgRunning = TRUE; - WHILE dbgRunning DO - MUTABLE dbgPrompt = "dbg> "; - writeFile("/dev/stderr", dbgPrompt); - MUTABLE dbgCmd = readLine!(); - IF eql?(dbgCmd, ":c") || eql?(dbgCmd, ":continue") THEN - dbgRunning = FALSE; - ELSE_IF eql?(dbgCmd, ":stack") || eql?(dbgCmd, ":s") THEN - print(" value stack (sp=" + toString(sp) + "):"); - FOR di IN (0_i64 ..< sp) DO - print(" [" + toString(di) + "] " + prStr(stack[di], TRUE)); - END - ELSE_IF eql?(dbgCmd, ":istack") THEN - print(" istack (isp=" + toString(isp) + "):"); - FOR di IN (0_i64 ..< isp) DO - print(" [" + toString(di) + "] " + toString(istack[di])); - END - ELSE_IF eql?(dbgCmd, ":fstack") THEN - print(" fstack (fsp=" + toString(fsp) + "):"); - FOR di IN (0_i64 ..< fsp) DO - print(" [" + toString(di) + "] " + toString(fstack[di])); - END - ELSE_IF eql?(dbgCmd, ":locals") || eql?(dbgCmd, ":l") THEN - print(" slots:"); - FOR di IN (0_i64 ..< 64) DO - PARTIAL MATCH slots[di] START - Value.Nil ->, - DEFAULT -> - print(" [" + toString(di) + "] " + prStr(slots[di], TRUE)); - END - END - ELSE_IF eql?(dbgCmd, ":ip") THEN - print(" ip=" + toString(ip) + " op=" + toString(ops[ip])); - # Show next 5 opcodes - FOR di IN (0_i64 ..< 5) DO - IF ip + di < ops.length() THEN - print(" ip+" + toString(di) + ": " + toString(ops[ip + di])); - END - END - ELSE_IF eql?(dbgCmd, ":consts") THEN - print(" constants:"); - FOR di IN (0_i64 ..< consts.length()) DO - print(" [" + toString(di) + "] " + prStr(consts[di], TRUE)); - END - ELSE_IF eql?(dbgCmd, ":env") THEN - print(" (use :eval to inspect env bindings)"); - ELSE_IF startsWith?(dbgCmd, ":eval ") THEN - MUTABLE dbgExpr = substr(dbgCmd, 6, dbgCmd.length() - 6); - MUTABLE dbgPenv: HashMap = {}; - tokenizeToEnv!(dbgPenv, dbgExpr); - dbgPenv["__rp"] = Value{ Number: 0.0 }; - dbgAst = readFormEnv!(dbgPenv); - dbgResult = eval!(COPY dbgAst, curEnv, pool); - print(" => " + prStr(dbgResult, TRUE)); - ELSE_IF eql?(dbgCmd, ":help") || eql?(dbgCmd, ":h") || eql?(dbgCmd, "?") THEN - print(" :c continue execution"); - print(" :stack show value stack"); - print(" :istack show i64 stack"); - print(" :fstack show f64 stack"); - print(" :locals show non-nil slots"); - print(" :ip show instruction pointer + next ops"); - print(" :consts show constant pool"); - print(" :env show environment info"); - print(" :eval evaluate expression in current env"); - ELSE - print(" unknown command. type :help for commands"); - END - END, - 59 -> # LOAD_ISLOT [idx] (islots -> istack, no Value wrapping) - slotIdx = ops[ip]; ip += 1; - istack[isp] = islots[slotIdx]; - isp += 1;, - 60 -> # STORE_ISLOT [idx] (istack -> islots, no Value wrapping) - slotIdx = ops[ip]; ip += 1; - isp -= 1; - islots[slotIdx] = istack[isp];, - 61 -> # LOAD_FSLOT [idx] (fslots -> fstack, no Value wrapping) - slotIdx = ops[ip]; ip += 1; - fstack[fsp] = fslots[slotIdx]; - fsp += 1;, - 62 -> # STORE_FSLOT [idx] (fstack -> fslots, no Value wrapping) - slotIdx = ops[ip]; ip += 1; - fsp -= 1; - fslots[slotIdx] = fstack[fsp];, - 63 -> # STRUCT_FIELD [idx] (pop Value.Vector, push fields[idx]) - fieldIdx = ops[ip]; ip += 1; - sp -= 1; - PARTIAL MATCH stack[sp] START - Value.Vector AS fields -> pv = COPY fields[fieldIdx];, - DEFAULT -> pv = Value.Nil; - END - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 64 -> # TYPED_FIELD_I64 [idx] (pop TypedI64Arr, push Int64Val) - fieldIdx = ops[ip]; ip += 1; - sp -= 1; - PARTIAL MATCH stack[sp] START - Value.TypedI64Arr AS iarr -> pv = Value{ Int64Val: iarr[fieldIdx] };, - DEFAULT -> pv = Value.Nil; - END - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - 65 -> # TYPED_FIELD_F64 [idx] (pop TypedF64Arr, push Number) - fieldIdx = ops[ip]; ip += 1; - sp -= 1; - PARTIAL MATCH stack[sp] START - Value.TypedF64Arr AS farr -> pv = Value{ Number: farr[fieldIdx] };, - DEFAULT -> pv = Value.Nil; - END - IF sp >= stack.length() THEN stack.append(pv); ELSE stack[sp] = pv; END - sp += 1;, - DEFAULT -> - running = FALSE; - END - END - - IF sp > 0 THEN - RETURN COPY stack[sp - 1]; - END - RETURN Value.Nil; -END diff --git a/examples/minivm/interpreter_test.cht b/examples/minivm/interpreter_test.cht deleted file mode 100644 index 2c0c77732..000000000 --- a/examples/minivm/interpreter_test.cht +++ /dev/null @@ -1,272 +0,0 @@ -# Tests for the minivm interpreter. -# Covers tree-walk eval, bytecode exec, typed arrays, FFI, and struct field opcodes. - -REQUIRE "types.cht"; -REQUIRE "parser.cht"; -REQUIRE "debugger.cht"; -REQUIRE "interpreter.cht"; - -# Test helpers: create fresh interpreter, run input, return result string. - -FN evalIn!(input: String, readable: Bool, rootId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS String EFFECTS REENTRANT -> - val = runTest!(input, rootId, pool, penv); - RETURN COPY prStr(val, readable); -END - -FN evalSeqIn!(inputs: String[], readable: Bool, rootId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS String EFFECTS REENTRANT -> - MUTABLE result: Value = Value.Nil; - FOR i IN (0_i64 ..< inputs.length()) DO - result = runTest!(inputs[i], rootId, pool, penv); - END - RETURN prStr(result, readable); -END - -# compileAndExec!: parse S-expr, compile to bytecode, execute. -# Uses the tree-walker's eval! to run compile! (which is CLEAR code), -# then extracts the result and runs it on exec!. - -FN bcEvalIn!(input: String, readable: Bool, rootId: Id, MUTABLE pool: Env[50000]@pool, MUTABLE penv: HashMap) RETURNS String EFFECTS REENTRANT -> - tokenizeToEnv!(penv, input); - penv["__rp"] = Value{ Number: 0.0 }; - ast = readFormEnv!(penv); - compile!(ast, rootId, pool); - # Read compiled bytecode from env (stored entry-by-entry) - opCount = toInt(getNum(pool[rootId]?.vars["__bc_opN"] OR Value{ Number: 0.0 })); - constCount = toInt(getNum(pool[rootId]?.vars["__bc_cN"] OR Value{ Number: 0.0 })); - MUTABLE intOps: Int64[]@list = List[]; - FOR oi IN (0_i64 ..< opCount) DO - opV = pool[rootId]?.vars["__bc_o" + oi.toString()] OR Value{ Number: 0.0 }; - intOps.append(toInt(getNum(opV))); - END - MUTABLE bcConsts: Value[]@list = List[]; - FOR ci IN (0_i64 ..< constCount) DO - cV = pool[rootId]?.vars["__bc_c" + ci.toString()] OR Value.Nil; - bcConsts.append(cV); - END - result = exec!(intOps, bcConsts, rootId, pool); - RETURN prStr(result, readable); -END - -FN testBytecode!(rootId: Id, MUTABLE pool: Env[50000]@pool) RETURNS Void EFFECTS REENTRANT -> - # Test 1: (10 - 4) * 3 = 18 (arithmetic + stack ops) - MUTABLE ops1: Int64[]@list = List[]; - MUTABLE c1: Value[]@list = List[]; - c1.append(Value{ Number: 10.0 }); c1.append(Value{ Number: 4.0 }); c1.append(Value{ Number: 3.0 }); - ops1.append(0); ops1.append(0); # LOAD_CONST 10 - ops1.append(0); ops1.append(1); # LOAD_CONST 4 - ops1.append(5); # SUB -> 6 - ops1.append(0); ops1.append(2); # LOAD_CONST 3 - ops1.append(6); # MUL -> 18 - ops1.append(19); # HALT - ASSERT prStr(exec!(ops1, c1, rootId, pool), TRUE) == "18", "bc: (10-4)*3 = 18"; - - # Test 2: if 1 < 2 then 10 else 20 (control flow) - MUTABLE ops2: Int64[]@list = List[]; - MUTABLE c2: Value[]@list = List[]; - c2.append(Value{ Number: 1.0 }); c2.append(Value{ Number: 2.0 }); - c2.append(Value{ Number: 10.0 }); c2.append(Value{ Number: 20.0 }); - ops2.append(0); ops2.append(0); # LOAD_CONST 1 - ops2.append(0); ops2.append(1); # LOAD_CONST 2 - ops2.append(9); # LT - ops2.append(15); ops2.append(11); # JUMP_IF_FALSE -> 11 - ops2.append(0); ops2.append(2); # LOAD_CONST 10 (then) - ops2.append(14); ops2.append(13); # JUMP -> 13 - ops2.append(0); ops2.append(3); # LOAD_CONST 20 (else) - ops2.append(19); # HALT - ASSERT prStr(exec!(ops2, c2, rootId, pool), TRUE) == "10", "bc: if 1<2 then 10 else 20"; - - # Test 3: define x=5, native +(x, 3) -> 8 (env + native calls) - MUTABLE ops3: Int64[]@list = List[]; - MUTABLE c3: Value[]@list = List[]; - c3.append(Value{ Number: 5.0 }); c3.append(Value{ Symbol: "x" }); c3.append(Value{ Number: 3.0 }); - ops3.append(0); ops3.append(0); # LOAD_CONST 5 - ops3.append(2); ops3.append(1); # STORE_NAME "x" - ops3.append(3); # POP - ops3.append(1); ops3.append(1); # LOAD_NAME "x" -> 5 - ops3.append(0); ops3.append(2); # LOAD_CONST 3 - ops3.append(18); ops3.append(1); ops3.append(2); # NATIVE_CALL +(id=1), argc=2 - ops3.append(19); # HALT - ASSERT prStr(exec!(ops3, c3, rootId, pool), TRUE) == "8", "bc: define x=5, x+3=8"; - - # Test 4: STRUCT_FIELD - untyped struct (Vector), access field 1 - # struct = #(10, 20, 30), field[1] = 20 - MUTABLE ops4: Int64[]@list = List[]; - MUTABLE c4: Value[]@list = List[]; - MUTABLE sf4: Value[]@list = List[]; - sf4.append(Value{ Number: 10.0 }); sf4.append(Value{ Number: 20.0 }); sf4.append(Value{ Number: 30.0 }); - c4.append(Value{ Vector: sf4 }); - ops4.append(0); ops4.append(0); # LOAD_CONST struct - ops4.append(63); ops4.append(1); # STRUCT_FIELD 1 -> 20 - ops4.append(19); # HALT - ASSERT prStr(exec!(ops4, c4, rootId, pool), TRUE) == "20", "bc: STRUCT_FIELD untyped"; - - # Test 5: TYPED_FIELD_I64 - typed i64 struct, access field 0 - # struct = [100, 200]:i64, field[0] = 100 - MUTABLE ops5: Int64[]@list = List[]; - MUTABLE c5: Value[]@list = List[]; - MUTABLE si5: Int64[]@list = List[]; - si5.append(100_i64); si5.append(200_i64); - c5.append(Value{ TypedI64Arr: si5 }); - ops5.append(0); ops5.append(0); # LOAD_CONST typed i64 arr - ops5.append(64); ops5.append(0); # TYPED_FIELD_I64 0 -> 100 - ops5.append(19); # HALT - ASSERT prStr(exec!(ops5, c5, rootId, pool), TRUE) == "100", "bc: TYPED_FIELD_I64"; - - # Test 6: TYPED_FIELD_F64 - typed f64 struct, access field 1 - # struct = [1.5, 2.5]:f64, field[1] = 2.5 - MUTABLE ops6: Int64[]@list = List[]; - MUTABLE c6: Value[]@list = List[]; - MUTABLE sf6: Float64[]@list = List[]; - sf6.append(1.5); sf6.append(2.5); - c6.append(Value{ TypedF64Arr: sf6 }); - ops6.append(0); ops6.append(0); # LOAD_CONST typed f64 arr - ops6.append(65); ops6.append(1); # TYPED_FIELD_F64 1 -> 2.5 - ops6.append(19); # HALT - ASSERT prStr(exec!(ops6, c6, rootId, pool), TRUE) == "2", "bc: TYPED_FIELD_F64"; - - # Test 7: Nested - untyped struct with typed i64 struct in field 1 - # outer = #("tag", [10, 20, 30]:i64), outer.field[1].field[2] = 30 - MUTABLE ops7: Int64[]@list = List[]; - MUTABLE c7: Value[]@list = List[]; - MUTABLE si7: Int64[]@list = List[]; - si7.append(10_i64); si7.append(20_i64); si7.append(30_i64); - MUTABLE sf7: Value[]@list = List[]; - sf7.append(Value{ Str: "Point3D" }); sf7.append(Value{ TypedI64Arr: si7 }); - c7.append(Value{ Vector: sf7 }); - ops7.append(0); ops7.append(0); # LOAD_CONST outer struct - ops7.append(63); ops7.append(1); # STRUCT_FIELD 1 -> TypedI64Arr [10,20,30] - ops7.append(64); ops7.append(2); # TYPED_FIELD_I64 2 -> 30 - ops7.append(19); # HALT - ASSERT prStr(exec!(ops7, c7, rootId, pool), TRUE) == "30", "bc: nested STRUCT_FIELD -> TYPED_FIELD_I64"; - - RETURN; -END - -FN main() RETURNS Void -> - MUTABLE pool: Env[50000]@pool = []; - MUTABLE penv: HashMap = {}; - rootId = setupEnv!(pool); - - # Arithmetic - ASSERT evalIn!("(+ 1 2)", TRUE, rootId, pool, penv) == "3", "(+ 1 2)"; - ASSERT evalIn!("(- 5 3)", TRUE, rootId, pool, penv) =="2", "(- 5 3)"; - ASSERT evalIn!("(* 3 4)", TRUE, rootId, pool, penv) =="12", "(* 3 4)"; - ASSERT evalIn!("(/ 10 2)", TRUE, rootId, pool, penv) =="5", "(/ 10 2)"; - ASSERT evalIn!("(+ (* 2 3) (- 10 4))", TRUE, rootId, pool, penv) =="12", "nested arithmetic"; - - # Variables - ASSERT evalIn!("(def! a 10)", TRUE, rootId, pool, penv) =="10", "def! returns value"; - ASSERT evalSeqIn!(["(def! a 10)", "a"], TRUE, rootId, pool, penv) =="10", "def! is retrievable"; - - # Closures - ASSERT evalIn!("(def! f (fn* (x) (+ x 1)))", TRUE, rootId, pool, penv) =="#", "fn* creates function"; - ASSERT evalSeqIn!(["(def! a 10)", "(def! f (fn* (x) (+ x a)))", "(f 5)"], TRUE, rootId, pool, penv) =="15", "closure captures outer"; - ASSERT evalSeqIn!(["(def! add3 (fn* (a b c) (+ a (+ b c))))", "(add3 1 2 3)"], TRUE, rootId, pool, penv) =="6", "multi-param fn"; - - # Let bindings - ASSERT evalIn!("(let* (b 2 c 3) (+ b c))", TRUE, rootId, pool, penv) =="5", "let* scoped bindings"; - ASSERT evalIn!("(let* (a 1 b (+ a 1)) b)", TRUE, rootId, pool, penv) =="2", "let* references earlier"; - - # Conditionals - ASSERT evalIn!("(if true 7 8)", TRUE, rootId, pool, penv) =="7", "if true"; - ASSERT evalIn!("(if false 7 8)", TRUE, rootId, pool, penv) =="8", "if false"; - ASSERT evalIn!("(if nil 7 8)", TRUE, rootId, pool, penv) =="8", "if nil"; - ASSERT evalIn!("(if false 7)", TRUE, rootId, pool, penv) =="nil", "if without else"; - - # Sequential evaluation - ASSERT evalIn!("(do (def! d 6) 7 (+ d 8))", TRUE, rootId, pool, penv) =="14", "do returns last"; - - # Comparison - ASSERT evalIn!("(= 1 1)", TRUE, rootId, pool, penv) =="true", "= equal"; - ASSERT evalIn!("(= 1 2)", TRUE, rootId, pool, penv) =="false", "= unequal"; - ASSERT evalIn!("(> 2 1)", TRUE, rootId, pool, penv) =="true", "> comparison"; - ASSERT evalIn!("(< 1 2)", TRUE, rootId, pool, penv) =="true", "< comparison"; - ASSERT evalIn!("(<= 2 2)", TRUE, rootId, pool, penv) =="true", "<= equal"; - ASSERT evalIn!("(>= 3 2)", TRUE, rootId, pool, penv) =="true", ">= greater"; - - # List operations - ASSERT evalIn!("(list 1 2 3)", FALSE, rootId, pool, penv) == "(1 2 3)", "list creates list"; - ASSERT evalIn!("(count (list 1 2 3))", TRUE, rootId, pool, penv) =="3", "count"; - ASSERT evalIn!("(empty? (list))", TRUE, rootId, pool, penv) =="true", "empty?"; - ASSERT evalIn!("(not false)", TRUE, rootId, pool, penv) =="true", "not"; - ASSERT evalIn!("(list? (list 1 2))", TRUE, rootId, pool, penv) =="true", "list?"; - - # Recursion - ASSERT evalSeqIn!(["(def! sumdown (fn* (n) (if (> n 0) (+ n (sumdown (- n 1))) 0)))", "(sumdown 6)"], TRUE, rootId, pool, penv) =="21", "sumdown"; - ASSERT evalSeqIn!(["(def! fib (fn* (n) (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2))))))", "(fib 4)"], TRUE, rootId, pool, penv) =="5", "fibonacci"; - - # Scheme syntax: define, lambda, let, begin - ASSERT evalIn!("(define x 42)", TRUE, rootId, pool, penv) =="42", "define"; - ASSERT evalSeqIn!(["(define x 42)", "x"], TRUE, rootId, pool, penv) =="42", "define retrievable"; - ASSERT evalSeqIn!(["(define inc (lambda (n) (+ n 1)))", "(inc 5)"], TRUE, rootId, pool, penv) =="6", "lambda"; - ASSERT evalIn!("(let ((a 3) (b 4)) (+ a b))", TRUE, rootId, pool, penv) =="7", "let"; - ASSERT evalIn!("(begin 1 2 3)", TRUE, rootId, pool, penv) =="3", "begin"; - - # set!: mutable reassignment - ASSERT evalSeqIn!(["(define x 1)", "(set! x 2)", "x"], TRUE, rootId, pool, penv) =="2", "set! reassigns"; - ASSERT evalSeqIn!(["(define x 10)", "(define inc! (lambda () (set! x (+ x 1))))", "(inc!)", "x"], TRUE, rootId, pool, penv) =="11", "set! in closure"; - - # Smoke tests (full suite moved to run_tests.rb to avoid stack overflow) - ASSERT evalIn!("(+ 1 2)", TRUE, rootId, pool, penv) == "3", "add"; - ASSERT evalIn!("(if true 1 2)", TRUE, rootId, pool, penv) == "1", "if"; - ASSERT evalSeqIn!(["(define f (lambda (x) (* x 2)))", "(f 21)"], TRUE, rootId, pool, penv) == "42", "lambda"; - ASSERT evalIn!("(try (raise \"x\" \"E\") (catch e \"ok\"))", FALSE, rootId, pool, penv) == "ok", "try/catch"; - - # Typed values: Int64Val - ASSERT evalIn!("42:i64", TRUE, rootId, pool, penv) == "42", "typed i64 literal"; - ASSERT evalIn!("(+ 10:i64 20:i64)", TRUE, rootId, pool, penv) == "30", "typed i64 add"; - ASSERT evalIn!("(= 42:i64 42)", TRUE, rootId, pool, penv) == "true", "i64 == float cross-type"; - ASSERT evalIn!("(= 42 42:i64)", TRUE, rootId, pool, penv) == "true", "float == i64 cross-type"; - ASSERT evalIn!("(type-of 42:i64)", FALSE, rootId, pool, penv) == "Int64", "type-of i64"; - ASSERT evalIn!("(type-of 42)", FALSE, rootId, pool, penv) == "Float64", "type-of untyped"; - ASSERT evalIn!("(+ 10:i64 2.5)", TRUE, rootId, pool, penv) == "12", "i64 + float promotes"; - ASSERT evalIn!("(* 3:i64 4:i64)", TRUE, rootId, pool, penv) == "12", "typed i64 mul"; - ASSERT evalIn!("(- 10:i64 3:i64)", TRUE, rootId, pool, penv) == "7", "typed i64 sub"; - - # Typed arrays: TypedI64Arr - ASSERT evalIn!("(typed-list:i64 10 20 30)", TRUE, rootId, pool, penv) == "[10 20 30]:i64", "typed i64 array create"; - ASSERT evalIn!("(list-ref (typed-list:i64 10 20 30) 1)", TRUE, rootId, pool, penv) == "20", "typed array ref"; - ASSERT evalIn!("(type-of (list-ref (typed-list:i64 10 20 30) 0))", FALSE, rootId, pool, penv) == "Int64", "typed array element type"; - ASSERT evalIn!("(list-length (typed-list:i64 1 2 3 4))", TRUE, rootId, pool, penv) == "4", "typed array length"; - # Conversion - ASSERT evalIn!("(list-ref (to-list (typed-list:i64 5 10)) 0)", TRUE, rootId, pool, penv) == "5", "to-list conversion"; - ASSERT evalIn!("(list-ref (to-typed:i64 (list 5 10)) 0)", TRUE, rootId, pool, penv) == "5", "to-typed conversion"; - # Interop: typed values in untyped operations - ASSERT evalIn!("(+ (list-ref (typed-list:i64 10 20) 0) 5)", TRUE, rootId, pool, penv) == "15", "typed arr elem + untyped"; - - # list-ref on Value.List of strings must return an owned copy (UAF regression) - # Bug: listRef returned items[idx] without COPY; evaled freed on evalList! exit - # leaving a dangling pointer that crashed dupeUnionValue when the value was reused. - ASSERT evalSeqIn!([ - "(define ws (split \"apple banana cherry\" \" \"))", - "(define w (list-ref ws 0))", - "(define w2 (list-ref ws 1))", - "(define w3 (list-ref ws 2))", - "(eq? w \"apple\")" - ], TRUE, rootId, pool, penv) == "true", "list-ref string element is owned copy (UAF regression)"; - - # FFI bridge: native CLEAR functions on typed arrays - ASSERT evalIn!("(native-sum (typed-list:i64 10 20 30))", TRUE, rootId, pool, penv) == "60", "FFI native-sum i64"; - ASSERT evalIn!("(type-of (native-sum (typed-list:i64 1 2 3)))", FALSE, rootId, pool, penv) == "Int64", "FFI returns Int64"; - ASSERT evalIn!("(native-contains (typed-list:i64 10 20 30) 20)", TRUE, rootId, pool, penv) == "true", "FFI contains found"; - ASSERT evalIn!("(native-contains (typed-list:i64 10 20 30) 99)", TRUE, rootId, pool, penv) == "false", "FFI contains not found"; - # FFI f64 - ASSERT evalIn!("(native-sum-f64 (typed-list:f64 1.5 2.5 3.0))", TRUE, rootId, pool, penv) == "7", "FFI native-sum-f64"; - ASSERT evalIn!("(native-dot (typed-list:f64 1 2 3) (typed-list:f64 4 5 6))", TRUE, rootId, pool, penv) == "32", "FFI native-dot"; - # Conversions - ASSERT evalIn!("(type-of (int->float 42:i64))", FALSE, rootId, pool, penv) == "Float64", "int->float type"; - ASSERT evalIn!("(type-of (float->int 3.14))", FALSE, rootId, pool, penv) == "Int64", "float->int type"; - ASSERT evalIn!("(float->int 42.9)", TRUE, rootId, pool, penv) == "42", "float->int truncates"; - - # Bytecode VM tests (includes struct field opcodes) - testBytecode!(rootId, pool); - - # Typed structs: BLOCKED by compiler bug #2 (arena lifetime). - # TypedI64Arr inside Pair{@indirect} gets freed when the special form handler returns. - # The construction + access special forms are implemented but data doesn't survive. - # Fix: PromotionPlan needs to promote Int64[]@list to heap when escaping via @indirect. - # Tests disabled until compiler fix: - - print("Interpreter tests PASSED!"); - RETURN; -END diff --git a/examples/minivm/run_tests.rb b/examples/minivm/run_tests.rb index e4a5948ea..bfc2d2ffb 100644 --- a/examples/minivm/run_tests.rb +++ b/examples/minivm/run_tests.rb @@ -1,12 +1,11 @@ #!/usr/bin/env ruby # MiniVM runner policy: -# - The primary correctness target is interpreter_test.cht. -# - The broader transpile-tests runner is historical/aspirational coverage. +# - The primary correctness target is the bytecode VM against transpile-tests. +# - Use --vm-coverage for the full supportable-test coverage report. MINIVM_CLEAR = File.join(__dir__, "clear") TRANSPILER = File.join(__dir__, "bc_run.rb") TEST_DIR = File.expand_path("../../transpile-tests", __dir__) -INTERPRETER_TEST = File.join(__dir__, "interpreter_test.cht") HISTORICAL_KNOWN_PASSING = %w[ 01_stack_alloc @@ -117,11 +116,6 @@ "217_loop_carry_overflow_blocks" => :slow_stress_test, } -def run_primary_test - system(MINIVM_CLEAR, "test", INTERPRETER_TEST) - $?.exitstatus || 1 -end - def run_historical_test(path) # Use a short kill-after so infinite-loop tests don't hang the runner. output = `timeout --kill-after=2 10 ruby #{TRANSPILER} #{path} --run 2>&1` @@ -249,10 +243,10 @@ def run_vm_coverage def usage puts "Usage:" puts " ruby examples/minivm/run_tests.rb" - puts " Runs the primary MiniVM regression target: interpreter_test.cht" + puts " Runs the known-passing transpile-tests through bc_run" puts puts " ruby examples/minivm/run_tests.rb --historical" - puts " Runs the broader historical transpile-tests coverage" + puts " Same as the default known-passing transpile-tests run" puts puts " ruby examples/minivm/run_tests.rb --all" puts " Runs the historical known-passing list plus additional candidates" @@ -269,7 +263,8 @@ def usage end if ARGV.empty? - exit(run_primary_test) + ok = run_historical_suite(HISTORICAL_KNOWN_PASSING, "Known-Passing Transpile Tests") + exit(ok ? 0 : 1) elsif ARGV[0] == "--historical" ok = run_historical_suite(HISTORICAL_KNOWN_PASSING, "Historical Known-Passing Tests") exit(ok ? 0 : 1) From 7519f7d2d45022f291305e5d2b7924df56e928c9 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 15:51:43 +0000 Subject: [PATCH 2/8] fix(fmt): preserve mutable bang-call args --- spec/clear_fmt_spec.rb | 18 ++++++++++ spec/lint_fix_rewriter_spec.rb | 17 ++++++++++ src/tools/lint_fix_rewriter.rb | 62 +++++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/spec/clear_fmt_spec.rb b/spec/clear_fmt_spec.rb index 2c680d4b7..54e49038d 100644 --- a/spec/clear_fmt_spec.rb +++ b/spec/clear_fmt_spec.rb @@ -128,6 +128,24 @@ def write(name, content) expect(out).to eq("FN f() RETURNS Int64 ->\n RETURN 0;\nEND\n") end + it "keeps MUTABLE on bindings passed to bang helpers" do + src = <<~CLEAR + FN appendOne!(MUTABLE xs: Int64[]@list) RETURNS Void -> + xs.append(1_i64); + RETURN; + END + + FN main() RETURNS Void -> + MUTABLE xs: Int64[]@list = []; + appendOne!(xs); + RETURN; + END + CLEAR + path = write("bang_mutable.cht", src) + out, _, _ = run_fmt("--stdout", path) + expect(out).to include("MUTABLE xs: Int64[]@list = []") + end + it "wraps FN signature when it exceeds 120 chars" do long = (1..6).map { |i| "p#{i}: SomeReallyLongTypeName" }.join(", ") path = write("l.cht", "FN withLotsOfParams(#{long}) RETURNS Int64 ->\n RETURN 0;\nEND\n") diff --git a/spec/lint_fix_rewriter_spec.rb b/spec/lint_fix_rewriter_spec.rb index 992ec590b..77d9cd09a 100644 --- a/spec/lint_fix_rewriter_spec.rb +++ b/spec/lint_fix_rewriter_spec.rb @@ -155,6 +155,23 @@ def rw(src) expect(out).to include("n = 42;") expect(out).not_to include("MUTABLE") end + + it "keeps MUTABLE when the binding is passed to a bang helper" do + src = <<~CLEAR + FN appendOne!(MUTABLE xs: Int64[]@list) RETURNS Void -> + xs.append(1_i64); + RETURN; + END + + FN main() RETURNS Void -> + MUTABLE xs: Int64[]@list = []; + appendOne!(xs); + RETURN; + END + CLEAR + out = rw(src) + expect(out).to include("MUTABLE xs: Int64[]@list = []") + end end describe "redundant `: Type` annotation drop — sync awareness" do diff --git a/src/tools/lint_fix_rewriter.rb b/src/tools/lint_fix_rewriter.rb index ee2df4934..4055c450b 100644 --- a/src/tools/lint_fix_rewriter.rb +++ b/src/tools/lint_fix_rewriter.rb @@ -37,8 +37,9 @@ def rewrite(source) ast, findings = annotate(source) return source unless ast bg_names = collect_bg_referenced_names(ast) + mutation_sensitive_names = collect_mutation_sensitive_names(ast) edits = [] - edits.concat(mutable_unused_edits(findings, bg_names)) + edits.concat(mutable_unused_edits(findings, bg_names, mutation_sensitive_names)) edits.concat(redundant_type_annotation_edits(ast, source)) return source if edits.empty? apply_edits(source, edits) @@ -72,6 +73,50 @@ def walk_for_bg_names(node, in_bg, set) node.each_pair { |_, v| walk_for_bg_names(v, inside, set) } end + def collect_mutation_sensitive_names(ast) + set = Set.new + walk_for_mutation_sensitive_names(ast, set) + set + end + + def walk_for_mutation_sensitive_names(node, set) + return if terminal?(node) + if node.is_a?(Array) + node.each { |n| walk_for_mutation_sensitive_names(n, set) } + return + end + + if node.is_a?(AST::FuncCall) && node.name.end_with?("!") + node.args.each { |arg| collect_identifier_names(arg, set) } + elsif node.is_a?(AST::MethodCall) && mutating_method_name?(node.name) + collect_identifier_names(node.object, set) + end + + return unless node.respond_to?(:each_pair) + node.each_pair { |_, v| walk_for_mutation_sensitive_names(v, set) } + end + + def collect_identifier_names(node, set) + return if terminal?(node) + if node.is_a?(Array) + node.each { |n| collect_identifier_names(n, set) } + return + end + if node.is_a?(AST::Identifier) && node.respond_to?(:name) + set << node.name + return + end + return unless node.respond_to?(:each_pair) + node.each_pair { |_, v| collect_identifier_names(v, set) } + end + + def mutating_method_name?(name) + %w[ + append clear delete insert pop push remove reserve resize set shift + swap truncate unshift + ].include?(name.to_s) + end + # Run the annotator with FixCollector enabled. Returns # [annotated_ast, findings] on success; [nil, []] if anything # raised. Errors are swallowed because fmt must remain robust @@ -95,10 +140,11 @@ def annotate(source) # ---- Rule 1: MUTABLE never reassigned ---- - def mutable_unused_edits(findings, bg_names) + def mutable_unused_edits(findings, bg_names, mutation_sensitive_names) findings.flat_map do |finding| next [] unless mutable_unused_finding?(finding) - next [] if mentions_bg_referenced_name?(finding, bg_names) + next [] if mentions_name_in_set?(finding, bg_names) + next [] if mentions_name_in_set?(finding, mutation_sensitive_names) finding.fixes .select { |fx| fx.confidence == :auto } .flat_map(&:edits) @@ -112,14 +158,14 @@ def mutable_unused_finding?(finding) end # Pull the binding name out of the finding's message - # ("MUTABLE 'name' is never reassigned ...") and check it against - # the set of names referenced inside any BG block. - def mentions_bg_referenced_name?(finding, bg_names) - return false if bg_names.empty? + # ("MUTABLE 'name' is never reassigned ...") and check it against a + # set of names where dropping MUTABLE would be unsafe. + def mentions_name_in_set?(finding, names) + return false if names.empty? msg = finding.respond_to?(:message) ? finding.message.to_s : "" m = msg.match(/MUTABLE '([^']+)'/) return false unless m - bg_names.include?(m[1]) + names.include?(m[1]) end # Translate a Span/Edit (1-based line/col) into a flat byte-offset From fddd91dcb641812fb8cf4d2605819a7f493b7c57 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 15:56:21 +0000 Subject: [PATCH 3/8] examples: add clear game of life --- examples/game-of-life/grid.cht | 123 +++++++++++++ examples/game-of-life/gui.cht | 79 +++++++++ examples/game-of-life/life-tests.cht | 31 ++++ examples/game-of-life/life.cht | 38 ++++ examples/game-of-life/ruby/Gemfile | 3 + examples/game-of-life/ruby/Gemfile.lock | 26 +++ examples/game-of-life/ruby/README.md | 14 ++ examples/game-of-life/ruby/glider.dat | 5 + examples/game-of-life/ruby/grid.rb | 50 ++++++ examples/game-of-life/ruby/gui.rb | 23 +++ examples/game-of-life/ruby/license.txt | 9 + examples/game-of-life/ruby/life.rb | 24 +++ examples/game-of-life/ruby/spec/grid_spec.rb | 174 +++++++++++++++++++ 13 files changed, 599 insertions(+) create mode 100644 examples/game-of-life/grid.cht create mode 100644 examples/game-of-life/gui.cht create mode 100644 examples/game-of-life/life-tests.cht create mode 100644 examples/game-of-life/life.cht create mode 100644 examples/game-of-life/ruby/Gemfile create mode 100644 examples/game-of-life/ruby/Gemfile.lock create mode 100644 examples/game-of-life/ruby/README.md create mode 100644 examples/game-of-life/ruby/glider.dat create mode 100644 examples/game-of-life/ruby/grid.rb create mode 100644 examples/game-of-life/ruby/gui.rb create mode 100644 examples/game-of-life/ruby/license.txt create mode 100755 examples/game-of-life/ruby/life.rb create mode 100644 examples/game-of-life/ruby/spec/grid_spec.rb diff --git a/examples/game-of-life/grid.cht b/examples/game-of-life/grid.cht new file mode 100644 index 000000000..bc28083a9 --- /dev/null +++ b/examples/game-of-life/grid.cht @@ -0,0 +1,123 @@ +STRUCT Cell { + x: Int64, + y: Int64 +} + +STRUCT CellState { + life: Bool, + neighbors: Int64 +} + +FN cellKey(cell: Cell) RETURNS !String -> + RETURN cell.x.toString() + "," + cell.y.toString(); +END + +FN sameCell?(a: Cell, b: Cell) RETURNS Bool -> + RETURN a.x == b.x && a.y == b.y; +END + +FN containsCell?(cells: Cell[]@list, cell: Cell) RETURNS Bool -> + MUTABLE i = 0; + WHILE i < cells.length() DO + IF sameCell?(cells[i], cell) -> RETURN TRUE; + i += 1; + END + RETURN FALSE; +END + +FN appendUniqueCell!(MUTABLE cells: Cell[]@list, cell: Cell) RETURNS !Void -> + IF containsCell?(cells, cell) == FALSE THEN + cells.append(cell); + END + RETURN; +END + +FN cellNeighbors(cell: Cell) RETURNS !Cell[]@list -> + MUTABLE neighbors: Cell[]@list = []; + MUTABLE dx = -1; + WHILE dx <= 1 DO + MUTABLE dy = -1; + WHILE dy <= 1 DO + IF dx != 0 || dy != 0 THEN + neighbors.append(Cell{ x: cell.x + dx, y: cell.y + dy }); + END + dy += 1; + END + dx += 1; + END + RETURN neighbors; +END + +FN buildGrid(liveCells: Cell[]@list) RETURNS !HashMap -> + MUTABLE states: HashMap = {}; + MUTABLE i = 0; + WHILE i < liveCells.length() DO + cell = liveCells[i]; + neighbors = cellNeighbors(cell); + + MUTABLE n = 0; + WHILE n < neighbors.length() DO + neighbor = neighbors[n]; + key = cellKey(neighbor); + MUTABLE state = states[key] OR CellState{ life: FALSE, neighbors: 0 }; + state.neighbors += 1; + states[key] = state; + n += 1; + END + + ownKey = cellKey(cell); + MUTABLE ownState = states[ownKey] OR CellState{ life: TRUE, neighbors: 0 }; + ownState.life = TRUE; + states[ownKey] = ownState; + i += 1; + END + RETURN states; +END + +FN revive?(lifeState: Bool, liveNeighbors: Int64) RETURNS Bool -> + RETURN lifeState == FALSE && liveNeighbors >= 3 && liveNeighbors <= 3; +END + +FN survive?(lifeState: Bool, liveNeighbors: Int64) RETURNS Bool -> + RETURN lifeState == TRUE && liveNeighbors >= 2 && liveNeighbors <= 3; +END + +FN evolve(state: CellState) RETURNS Bool -> + IF survive?(state.life, state.neighbors) || revive?(state.life, state.neighbors) THEN + RETURN TRUE; + END + RETURN FALSE; +END + +FN candidateCells(liveCells: Cell[]@list) RETURNS !Cell[]@list -> + MUTABLE candidates: Cell[]@list = []; + MUTABLE i = 0; + WHILE i < liveCells.length() DO + cell = liveCells[i]; + neighbors = cellNeighbors(cell); + MUTABLE n = 0; + WHILE n < neighbors.length() DO + appendUniqueCell!(candidates, neighbors[n]); + n += 1; + END + appendUniqueCell!(candidates, cell); + i += 1; + END + RETURN candidates; +END + +FN advance(liveCells: Cell[]@list) RETURNS !Cell[]@list -> + states = buildGrid(liveCells); + candidates = candidateCells(liveCells); + MUTABLE next: Cell[]@list = []; + MUTABLE i = 0; + WHILE i < candidates.length() DO + cell = candidates[i]; + state = states[cellKey(cell)] OR CellState{ life: FALSE, neighbors: 0 }; + IF evolve(state) THEN + next.append(cell); + END + i += 1; + END + RETURN next; +END diff --git a/examples/game-of-life/gui.cht b/examples/game-of-life/gui.cht new file mode 100644 index 000000000..bfe3f17ac --- /dev/null +++ b/examples/game-of-life/gui.cht @@ -0,0 +1,79 @@ +REQUIRE "grid.cht"; + +FN liveCellMap(cells: Cell[]@list) RETURNS !HashMap -> + MUTABLE live: HashMap = {}; + MUTABLE i = 0; + WHILE i < cells.length() DO + live[cellKey(cells[i])] = 1; + i += 1; + END + RETURN live; +END + +FN bounds(cells: Cell[]@list) RETURNS !Cell[]@list -> + MUTABLE result: Cell[]@list = []; + IF cells.empty?() THEN + result.append(Cell{ x: 0, y: 0 }); + result.append(Cell{ x: 0, y: 0 }); + RETURN result; + END + + MUTABLE minX = cells[0].x; + MUTABLE maxX = cells[0].x; + MUTABLE minY = cells[0].y; + MUTABLE maxY = cells[0].y; + MUTABLE i = 1; + WHILE i < cells.length() DO + IF cells[i].x < minX -> minX = cells[i].x; + IF cells[i].x > maxX -> maxX = cells[i].x; + IF cells[i].y < minY -> minY = cells[i].y; + IF cells[i].y > maxY -> maxY = cells[i].y; + i += 1; + END + + result.append(Cell{ x: minX, y: minY }); + result.append(Cell{ x: maxX, y: maxY }); + RETURN result; +END + +FN graph(cells: Cell[]@list) RETURNS !String[]@list -> + live = liveCellMap(cells); + b = bounds(cells); + min = b[0]; + max = b[1]; + MUTABLE rows: String[]@list = []; + + MUTABLE x = min.x - 1; + WHILE x <= max.x + 1 DO + MUTABLE row = ""; + MUTABLE y = min.y - 1; + WHILE y <= max.y + 1 DO + key = cellKey(Cell{ x: x, y: y }); + IF (live[key] OR 0) == 1 THEN + row = row + "X"; + ELSE + row = row + "O"; + END + y += 1; + END + rows.append(row); + x += 1; + END + + RETURN rows; +END + +FN render(cells: Cell[]@list) RETURNS !Void -> + b = bounds(cells); + min = b[0]; + max = b[1]; + print("---GENERATION---"); + print("(${min.x.toString()},${min.y.toString()}) (${max.x.toString()},${max.y.toString()})"); + rows = graph(cells); + MUTABLE i = 0; + WHILE i < rows.length() DO + print(rows[i]); + i += 1; + END + RETURN; +END diff --git a/examples/game-of-life/life-tests.cht b/examples/game-of-life/life-tests.cht new file mode 100644 index 000000000..5a0f03d09 --- /dev/null +++ b/examples/game-of-life/life-tests.cht @@ -0,0 +1,31 @@ +REQUIRE "grid.cht"; + +FN main() RETURNS Void -> + ASSERT survive?(TRUE, 4) == FALSE, "alive above population maximum dies"; + ASSERT survive?(TRUE, 1) == FALSE, "alive below population minimum dies"; + ASSERT survive?(TRUE, 3) == TRUE, "alive at population maximum survives"; + ASSERT survive?(TRUE, 2) == TRUE, "alive at population minimum survives"; + ASSERT survive?(TRUE, 2) == TRUE, "alive between population minimum and maximum survives"; + + ASSERT survive?(FALSE, 4) == FALSE, "dead above population maximum does not survive"; + ASSERT survive?(FALSE, 1) == FALSE, "dead below population minimum does not survive"; + ASSERT survive?(FALSE, 3) == FALSE, "dead at population maximum does not survive"; + ASSERT survive?(FALSE, 2) == FALSE, "dead at population minimum does not survive"; + ASSERT survive?(FALSE, 2) == FALSE, "dead between population minimum and maximum does not survive"; + + ASSERT revive?(TRUE, 4) == FALSE, "alive above reproduction count does not revive"; + ASSERT revive?(TRUE, 2) == FALSE, "alive below reproduction count does not revive"; + ASSERT revive?(TRUE, 3) == FALSE, "alive at reproduction count does not revive"; + ASSERT revive?(TRUE, 3) == FALSE, "alive at minimum reproduction count does not revive"; + ASSERT revive?(TRUE, 2) == FALSE, "alive between reproduction bounds does not revive"; + + ASSERT revive?(FALSE, 4) == FALSE, "dead above reproduction count does not revive"; + ASSERT revive?(FALSE, 2) == FALSE, "dead below reproduction count does not revive"; + ASSERT revive?(FALSE, 3) == TRUE, "dead at reproduction maximum revives"; + ASSERT revive?(FALSE, 3) == TRUE, "dead at reproduction minimum revives"; + + # Ruby spec marks the dead/between-population revive case pending because + # minimum and maximum are both 3, so there is no in-between value. + + RETURN; +END diff --git a/examples/game-of-life/life.cht b/examples/game-of-life/life.cht new file mode 100644 index 000000000..eaab226ca --- /dev/null +++ b/examples/game-of-life/life.cht @@ -0,0 +1,38 @@ +REQUIRE "grid.cht"; +REQUIRE "gui.cht"; + +FN parseCell(line: String) RETURNS !Cell -> + parts = line.split(","); + x = (parts[0].toNumber() OR 0.0).toInt(); + y = (parts[1].toNumber() OR 0.0).toInt(); + RETURN Cell{ x: x, y: y }; +END + +FN readGrid(path: String) RETURNS !Cell[]@list -> + raw = readFile(path) OR RAISE; + lines = raw.split("\n"); + MUTABLE cells: Cell[]@list = []; + MUTABLE i = 0; + WHILE i < lines.length() DO + line = lines[i].trim(); + IF line.any?() THEN + cells.append(parseCell(line)); + END + i += 1; + END + RETURN cells; +END + +FN runLife(path: String, generations: Int64) RETURNS !Void -> + MUTABLE cells = readGrid(path); + MUTABLE generation = 0; + WHILE generation < generations && cells.any?() DO + render(cells); + cells = advance(cells); + generation += 1; + END +END + +FN main() RETURNS Void -> + runLife("examples/game-of-life/ruby/glider.dat", 4) OR RAISE; +END diff --git a/examples/game-of-life/ruby/Gemfile b/examples/game-of-life/ruby/Gemfile new file mode 100644 index 000000000..4a05f8548 --- /dev/null +++ b/examples/game-of-life/ruby/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "rspec" diff --git a/examples/game-of-life/ruby/Gemfile.lock b/examples/game-of-life/ruby/Gemfile.lock new file mode 100644 index 000000000..77114569d --- /dev/null +++ b/examples/game-of-life/ruby/Gemfile.lock @@ -0,0 +1,26 @@ +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.3) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) + +PLATFORMS + ruby + +DEPENDENCIES + rspec + +BUNDLED WITH + 1.15.1 diff --git a/examples/game-of-life/ruby/README.md b/examples/game-of-life/ruby/README.md new file mode 100644 index 000000000..e54754b78 --- /dev/null +++ b/examples/game-of-life/ruby/README.md @@ -0,0 +1,14 @@ +# Conway's Game of Life + +Ruby implementation of The Game of Life. Inspired by reading [Understanding the Four Rules of Simple Design](https://leanpub.com/4rulesofsimpledesign). + +## Rules + +The universe of the Game of Life is an infinite two-dimensional orthogonal grid of square cells, each of which is in one of two possible states, alive or dead. Every cell interacts with its eight neighbours, which are the cells that are horizontally, vertically, or diagonally adjacent. At each step in time, the following transitions occur: + +* Any live cell with fewer than two live neighbours dies, as if caused by under-population. +* Any live cell with more than three live neighbours dies, as if by overcrowding. +* Any live cell with two or three live neighbours lives on to the next generation. +* Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. + +The initial pattern constitutes the seed of the system. The first generation is created by applying the above rules simultaneously to every cell in the seed—births and deaths occur simultaneously, and the discrete moment at which this happens is sometimes called a tick (in other words, each generation is a pure function of the preceding one). The rules continue to be applied repeatedly to create further generations. diff --git a/examples/game-of-life/ruby/glider.dat b/examples/game-of-life/ruby/glider.dat new file mode 100644 index 000000000..a959b5c19 --- /dev/null +++ b/examples/game-of-life/ruby/glider.dat @@ -0,0 +1,5 @@ +1,1 +1,2 +1,3 +2,1 +3,2 diff --git a/examples/game-of-life/ruby/grid.rb b/examples/game-of-life/ruby/grid.rb new file mode 100644 index 000000000..04d94bca8 --- /dev/null +++ b/examples/game-of-life/ruby/grid.rb @@ -0,0 +1,50 @@ +CellState = Struct.new(:life, :neighbors) + +def grid(live_cells) + live_cells + .reduce({}) do |acc, cell| + cell_neighbors(cell).each do |neighbor| + acc[neighbor] ||= CellState.new(false, 0) + acc[neighbor].neighbors += 1 + end + acc[cell] ||= CellState.new(true, 0) + acc[cell].life = true + acc + end +end + +def cell_neighbors(cell) + dimensions = cell.map { |coord| ((coord - 1)..(coord + 1)).to_a } + dimensions[0].product(*dimensions.drop(1)) - [cell] +end + +# TODO: pass these rules in to revive?/survive? +def revive?(life_state, live_neighbors) + life_state == false && live_neighbors >= 3 && live_neighbors <= 3 +end + +def survive?(life_state, live_neighbors) + life_state == true && live_neighbors >= 2 && live_neighbors <= 3 +end + +# @return bool +# cell's life state +def evolve(state) + return true if survive?(state.life, state.neighbors) || revive?(state.life, state.neighbors) + return false +end + +# @return array +# list of cells that lived to the next generation. +def advance(live_cells) + grid = grid(live_cells) + + grid + .reduce({}) do |acc, (cell, state)| + acc[cell] = evolve(state) + acc + end + .to_a + .select { |_cell, life_state| life_state } + .map(&:first) +end diff --git a/examples/game-of-life/ruby/gui.rb b/examples/game-of-life/ruby/gui.rb new file mode 100644 index 000000000..a62a9da84 --- /dev/null +++ b/examples/game-of-life/ruby/gui.rb @@ -0,0 +1,23 @@ +def render(cells) + grid = cells.reduce({}) do |acc, cell| + acc[cell] = 1 + acc + end + + xs = cells.map { |x, y| x } + ys = cells.map { |x, y| y } + + puts "---GENERATION---" + puts "(#{xs.min},#{ys.min}) (#{xs.max},#{ys.max})" + puts graph(grid, xs, ys) +end + +def graph(grid, xs, ys) + ((xs.min - 1)..(xs.max + 1)).to_a.reduce([]) do |acc, x| + acc << ((ys.min - 1)..(ys.max + 1)).to_a.reduce("") do |bcc, y| + bcc += grid.has_key?([x, y]) ? "X" : "O" + bcc + end + acc + end +end diff --git a/examples/game-of-life/ruby/license.txt b/examples/game-of-life/ruby/license.txt new file mode 100644 index 000000000..180d99e16 --- /dev/null +++ b/examples/game-of-life/ruby/license.txt @@ -0,0 +1,9 @@ +Copyright (c) 2018, Brian Yahn +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/examples/game-of-life/ruby/life.rb b/examples/game-of-life/ruby/life.rb new file mode 100755 index 000000000..633239640 --- /dev/null +++ b/examples/game-of-life/ruby/life.rb @@ -0,0 +1,24 @@ +#! /usr/bin/env ruby + +require_relative "grid" +require_relative "gui" + +def read_grid(file_path) + File + .read(file_path) + .lines + .map(&:chomp) + .map { |line| line.split(',').map(&:to_i) } +end + +def main() + cells = read_grid(ARGV.first) + + while cells.count > 0 + render(cells) + cells = advance(cells) + STDIN.gets + end +end + +main() diff --git a/examples/game-of-life/ruby/spec/grid_spec.rb b/examples/game-of-life/ruby/spec/grid_spec.rb new file mode 100644 index 000000000..c152ee367 --- /dev/null +++ b/examples/game-of-life/ruby/spec/grid_spec.rb @@ -0,0 +1,174 @@ +require_relative "../grid" + +describe "Grid" do + describe ".survive?" do + let(:minimum_population) { 2 } + let(:maximum_population) { 3 } + + subject { survive?(life_state, live_neighbors) } + + context "alive" do + let(:life_state) { true } + + context "above population maximum" do + let(:live_neighbors) { maximum_population + 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "below population minimum" do + let(:live_neighbors) { minimum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population maximum" do + let(:live_neighbors) { maximum_population } + it "is true" do + expect(subject).to eq(true) + end + end + + context "at population minimum" do + let(:live_neighbors) { minimum_population } + it "is true" do + expect(subject).to eq(true) + end + end + + context "between population minimum / maximum" do + let(:live_neighbors) { maximum_population - 1 } + it "is true" do + expect(subject).to eq(true) + end + end + end + + context "dead" do + let(:life_state) { false } + + context "above population maximum" do + let(:live_neighbors) { maximum_population + 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "below population minimum" do + let(:live_neighbors) { minimum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population maximum" do + let(:live_neighbors) { maximum_population } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population minimum" do + let(:live_neighbors) { minimum_population } + it "is true" do + expect(subject).to eq(false) + end + end + + context "between population minimum / maximum" do + let(:live_neighbors) { maximum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + end + end + + describe ".revive?" do + let(:minimum_population) { 3 } + let(:maximum_population) { 3 } + + subject { revive?(life_state, live_neighbors) } + + context "alive" do + let(:life_state) { true } + + context "above population maximum" do + let(:live_neighbors) { maximum_population + 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "below population minimum" do + let(:live_neighbors) { minimum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population maximum" do + let(:live_neighbors) { maximum_population } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population minimum" do + let(:live_neighbors) { minimum_population } + it "is true" do + expect(subject).to eq(false) + end + end + + context "between population minimum / maximum" do + let(:live_neighbors) { maximum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + end + + context "dead" do + let(:life_state) { false } + + context "above population maximum" do + let(:live_neighbors) { maximum_population + 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "below population minimum" do + let(:live_neighbors) { minimum_population - 1 } + it "is true" do + expect(subject).to eq(false) + end + end + + context "at population maximum" do + let(:live_neighbors) { maximum_population } + it "is true" do + expect(subject).to eq(true) + end + end + + context "at population minimum" do + let(:live_neighbors) { minimum_population } + it "is true" do + expect(subject).to eq(true) + end + end + + # TODO: pass rules in... + context "between population minimum / maximum" do + let(:live_neighbors) { maximum_population - 1 } + xit "is true" do + expect(subject).to eq(false) + end + end + end + end +end From ad3cea729fa398e0e07661b85712ff9cede818b0 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 16:19:59 +0000 Subject: [PATCH 4/8] examples: add brnfk interpreter --- examples/brnfk/brnfk-tests.cht | 52 ++++++++++ examples/brnfk/brnfk.cht | 169 +++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 examples/brnfk/brnfk-tests.cht create mode 100644 examples/brnfk/brnfk.cht diff --git a/examples/brnfk/brnfk-tests.cht b/examples/brnfk/brnfk-tests.cht new file mode 100644 index 000000000..d2de92251 --- /dev/null +++ b/examples/brnfk/brnfk-tests.cht @@ -0,0 +1,52 @@ +REQUIRE "brnfk.cht"; + +FN programErrors(program: String) RETURNS Bool -> + runWithLimits(program, "", 8, 1000) OR RAISE; + RETURN FALSE; + +CATCH Input + RETURN TRUE; + +DEFAULT + RETURN TRUE; +END + +FN programErrorsWithTape(program: String, tapeSize: Int64) RETURNS Bool -> + runWithLimits(program, "", tapeSize, 1000) OR RAISE; + RETURN FALSE; + +CATCH Input + RETURN TRUE; + +DEFAULT + RETURN TRUE; +END + +FN main() RETURNS Void -> + ASSERT incByte(255) == 0, "+ wraps 255 to 0"; + ASSERT decByte(0) == 255, "- wraps 0 to 255"; + ASSERT byteToString(65) == "A", "byteToString renders printable ASCII"; + ASSERT stringToByte("A") == 65, "stringToByte reads printable ASCII"; + ASSERT stringToByte("\n") == 10, "stringToByte reads newline"; + + ASSERT commands("a+b>c[.-],") == "+>[.-],", "parser ignores comments"; + + ASSERT run("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.", "") == "A", "prints A"; + ASSERT run(",.", "Z") == "Z", "echoes one byte"; + ASSERT run("+++++++++++++++++++++++++++++++++,. ", "") == "!", "EOF leaves cell unchanged"; + ASSERT run("+++++[>+++++++++++++<-]>.", "") == "A", "loop multiplies into next cell"; + + hello = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++." + + "<<+++++++++++++++.>.+++.------.--------.>+.>."; + ASSERT run(hello, "") == "Hello World!\n", "hello world program"; + + ASSERT runWithLimits("++>+++<", "", 8, 1000) == "", "limited runner returns output"; + + ASSERT programErrors("[") == TRUE, "unmatched [ errors"; + ASSERT programErrors("]") == TRUE, "unmatched ] errors"; + ASSERT programErrors("<") == TRUE, "pointer underflow errors"; + ASSERT programErrorsWithTape(">>", 2) == TRUE, "pointer overflow errors"; + ASSERT programErrorsWithTape("+[]", 8) == TRUE, "step limit errors"; + + RETURN; +END diff --git a/examples/brnfk/brnfk.cht b/examples/brnfk/brnfk.cht new file mode 100644 index 000000000..adbb30c85 --- /dev/null +++ b/examples/brnfk/brnfk.cht @@ -0,0 +1,169 @@ +FN printableAscii() RETURNS String -> + RETURN " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; +END + +FN incByte(n: Int64) RETURNS Int64 -> + IF n >= 255 THEN + RETURN 0; + END + RETURN n + 1; +END + +FN decByte(n: Int64) RETURNS Int64 -> + IF n <= 0 THEN + RETURN 255; + END + RETURN n - 1; +END + +FN byteToString(n: Int64) RETURNS !String -> + IF n == 10 THEN + RETURN "\n"; + END + IF n >= 32 && n <= 126 THEN + RETURN "" + printableAscii().charAt(n - 32); + END + RAISE "brnfk cannot render non-printable ASCII byte ${n.toString()}"; +END + +FN stringToByte(ch: String) RETURNS !Int64 -> + IF ch == "\n" THEN + RETURN 10; + END + + ascii = printableAscii(); + MUTABLE i = 0; + WHILE i < ascii.length() DO + IF ascii.charAt(i) == ch THEN + RETURN i + 32; + END + i += 1; + END + + RAISE "brnfk input contains unsupported non-printable character"; +END + +FN isCommand(ch: String) RETURNS Bool -> + RETURN ch == ">" || ch == "<" || ch == "+" || ch == "-" || + ch == "." || ch == "," || ch == "[" || ch == "]"; +END + +FN commands(program: String) RETURNS !String -> + MUTABLE parts: String[]@list = []; + MUTABLE i = 0; + WHILE i < program.length() DO + ch = program.charAt(i); + IF isCommand(ch) THEN + parts.append(ch); + END + i += 1; + END + RETURN parts.join(""); +END + +FN makeTape(size: Int64) RETURNS !Int64[]@list -> + IF size <= 0 THEN + RAISE "brnfk tape size must be positive"; + END + + MUTABLE tape: Int64[]@list = []; + tape.reserve(size); + MUTABLE i = 0; + WHILE i < size DO + tape.append(0); + i += 1; + END + RETURN tape; +END + +FN makeJumpTable(program: String) RETURNS !Int64[]@list -> + MUTABLE jumps: Int64[]@list = []; + jumps.reserve(program.length()); + MUTABLE i = 0; + WHILE i < program.length() DO + jumps.append(-1); + i += 1; + END + + MUTABLE stack: Int64[]@list = []; + i = 0; + WHILE i < program.length() DO + ch = program.charAt(i); + IF ch == "[" THEN + stack.append(i); + ELSE_IF ch == "]" THEN + open = stack.pop() OR -1; + IF open < 0 THEN + RAISE "brnfk unmatched ] at instruction ${i.toString()}"; + END + jumps[open] = i; + jumps[i] = open; + END + i += 1; + END + + IF stack.any?() THEN + open = stack.pop() OR -1; + RAISE "brnfk unmatched [ at instruction ${open.toString()}"; + END + + RETURN jumps; +END + +FN run(program: String, input: String) RETURNS !String -> + RETURN runWithLimits(program, input, 30_000, 10_000_000); +END + +FN runWithLimits(program: String, input: String, tapeSize: Int64, maxSteps: Int64) RETURNS !String -> + code = commands(program); + jumps = makeJumpTable(code); + MUTABLE tape = makeTape(tapeSize); + MUTABLE pointer = 0; + MUTABLE ip = 0; + MUTABLE inputPos = 0; + MUTABLE steps = 0; + MUTABLE output: String[]@list = []; + + WHILE ip < code.length() DO + IF steps >= maxSteps THEN + RAISE "brnfk exceeded max step count"; + END + + op = code.charAt(ip); + IF op == ">" THEN + pointer += 1; + IF pointer >= tape.length() THEN + RAISE "brnfk pointer moved past tape end"; + END + ELSE_IF op == "<" THEN + IF pointer == 0 THEN + RAISE "brnfk pointer moved before tape start"; + END + pointer -= 1; + ELSE_IF op == "+" THEN + tape[pointer] = incByte(tape[pointer]); + ELSE_IF op == "-" THEN + tape[pointer] = decByte(tape[pointer]); + ELSE_IF op == "." THEN + output.append(byteToString(tape[pointer])); + ELSE_IF op == "," THEN + IF inputPos < input.length() THEN + tape[pointer] = stringToByte(input.charAt(inputPos)); + inputPos += 1; + END + ELSE_IF op == "[" THEN + IF tape[pointer] == 0 THEN + ip = jumps[ip]; + END + ELSE_IF op == "]" THEN + IF tape[pointer] != 0 THEN + ip = jumps[ip]; + END + END + + ip += 1; + steps += 1; + END + + RETURN output.join(""); +END From 2f2131b0c6d2e6043b5c357166f3e38d98571d4b Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 16:39:42 +0000 Subject: [PATCH 5/8] examples: add brnfk corpus tests --- examples/brnfk/brnfk-corpus-tests.cht | 32 ++ examples/brnfk/brnfk.cht | 11 +- examples/brnfk/tests/392quine.b | 8 + examples/brnfk/tests/400quine.b | 8 + examples/brnfk/tests/README.md | 15 + examples/brnfk/tests/bounce.b | 172 +++++++++ examples/brnfk/tests/bsort.b | 14 + examples/brnfk/tests/chessboard.b | 97 +++++ examples/brnfk/tests/collatz.b | 33 ++ examples/brnfk/tests/dbf2c.b | 35 ++ examples/brnfk/tests/dbfi.b | 9 + examples/brnfk/tests/dquine.b | 15 + examples/brnfk/tests/dvorak.b | 16 + examples/brnfk/tests/e.b | 40 +++ examples/brnfk/tests/factorial.b | 7 + examples/brnfk/tests/factorial2.b | 24 ++ examples/brnfk/tests/fib.b | 9 + examples/brnfk/tests/ghost.b | 28 ++ examples/brnfk/tests/golden.b | 19 + examples/brnfk/tests/head.b | 7 + examples/brnfk/tests/impeccable.b | 18 + examples/brnfk/tests/isort.b | 11 + examples/brnfk/tests/jabh.b | 3 + examples/brnfk/tests/life.b | 38 ++ examples/brnfk/tests/null.b | 0 examples/brnfk/tests/numwarp.b | 34 ++ examples/brnfk/tests/qsort.b | 9 + examples/brnfk/tests/random.b | 14 + examples/brnfk/tests/rot13.b | 23 ++ examples/brnfk/tests/short.b | 75 ++++ examples/brnfk/tests/sierpinski.b | 12 + examples/brnfk/tests/squares.b | 8 + examples/brnfk/tests/squares2.b | 15 + examples/brnfk/tests/tests.b | 68 ++++ examples/brnfk/tests/thuemorse.b | 5 + examples/brnfk/tests/tictactoe.b | 52 +++ examples/brnfk/tests/utm.b | 491 ++++++++++++++++++++++++++ examples/brnfk/tests/wc.b | 13 + examples/brnfk/tests/xmastree.b | 8 + 39 files changed, 1495 insertions(+), 1 deletion(-) create mode 100644 examples/brnfk/brnfk-corpus-tests.cht create mode 100644 examples/brnfk/tests/392quine.b create mode 100644 examples/brnfk/tests/400quine.b create mode 100644 examples/brnfk/tests/README.md create mode 100644 examples/brnfk/tests/bounce.b create mode 100644 examples/brnfk/tests/bsort.b create mode 100644 examples/brnfk/tests/chessboard.b create mode 100644 examples/brnfk/tests/collatz.b create mode 100644 examples/brnfk/tests/dbf2c.b create mode 100644 examples/brnfk/tests/dbfi.b create mode 100644 examples/brnfk/tests/dquine.b create mode 100644 examples/brnfk/tests/dvorak.b create mode 100644 examples/brnfk/tests/e.b create mode 100644 examples/brnfk/tests/factorial.b create mode 100644 examples/brnfk/tests/factorial2.b create mode 100644 examples/brnfk/tests/fib.b create mode 100644 examples/brnfk/tests/ghost.b create mode 100644 examples/brnfk/tests/golden.b create mode 100644 examples/brnfk/tests/head.b create mode 100644 examples/brnfk/tests/impeccable.b create mode 100644 examples/brnfk/tests/isort.b create mode 100644 examples/brnfk/tests/jabh.b create mode 100644 examples/brnfk/tests/life.b create mode 100644 examples/brnfk/tests/null.b create mode 100644 examples/brnfk/tests/numwarp.b create mode 100644 examples/brnfk/tests/qsort.b create mode 100644 examples/brnfk/tests/random.b create mode 100644 examples/brnfk/tests/rot13.b create mode 100644 examples/brnfk/tests/short.b create mode 100644 examples/brnfk/tests/sierpinski.b create mode 100644 examples/brnfk/tests/squares.b create mode 100644 examples/brnfk/tests/squares2.b create mode 100644 examples/brnfk/tests/tests.b create mode 100644 examples/brnfk/tests/thuemorse.b create mode 100644 examples/brnfk/tests/tictactoe.b create mode 100644 examples/brnfk/tests/utm.b create mode 100644 examples/brnfk/tests/wc.b create mode 100644 examples/brnfk/tests/xmastree.b diff --git a/examples/brnfk/brnfk-corpus-tests.cht b/examples/brnfk/brnfk-corpus-tests.cht new file mode 100644 index 000000000..39e10ab7d --- /dev/null +++ b/examples/brnfk/brnfk-corpus-tests.cht @@ -0,0 +1,32 @@ +REQUIRE "brnfk.cht"; + +FN testPath(name: String) RETURNS !String -> + RETURN "../../examples/brnfk/tests/" + name; +END + +FN expectedSquares() RETURNS !String -> + MUTABLE lines: String[]@list = []; + MUTABLE i = 0; + WHILE i <= 100 DO + lines.append((i * i).toString()); + lines.append("\n"); + i += 1; + END + RETURN lines.join(""); +END + +FN main() RETURNS Void -> + ASSERT runFile(testPath("null.b") OR RAISE, "") == "", "null.b produces no output"; + ASSERT runFile(testPath("rot13.b") OR RAISE, "~mlk zyx") == "~zyx mlk", "rot13.b transforms known input"; + ASSERT runFile(testPath("xmastree.b") OR RAISE, "") == "*\n", "xmastree.b smoke output"; + ASSERT runFile(testPath("squares.b") OR RAISE, "") == expectedSquares(), "squares.b prints 0..100 squared"; + + ioProbe = ">,>+++++++++,>+++++++++++[<++++++<++++++<+>>>-]<<.>.<<-.>.>.<<."; + ASSERT runWithLimits(ioProbe, "\n", 30_000, 100_000) == "LK\nLK\n", "Cristofani newline/EOF probe"; + + obscureProbe = "[]++++++++++[>>+>+>++++++[<<+<+++>>>-]<<<<-]" + + "\"A*$\";?@![#>>+<<]>[>>]<<<<[>++<[-]]>.>."; + ASSERT runWithLimits(obscureProbe, "", 30_000, 1_000_000) == "H\n", "Cristofani obscure parser probe"; + + RETURN; +END diff --git a/examples/brnfk/brnfk.cht b/examples/brnfk/brnfk.cht index adbb30c85..2d720f16f 100644 --- a/examples/brnfk/brnfk.cht +++ b/examples/brnfk/brnfk.cht @@ -23,7 +23,7 @@ FN byteToString(n: Int64) RETURNS !String -> IF n >= 32 && n <= 126 THEN RETURN "" + printableAscii().charAt(n - 32); END - RAISE "brnfk cannot render non-printable ASCII byte ${n.toString()}"; + RAISE "brnfk cannot render non-printable byte ${n.toString()} as String"; END FN stringToByte(ch: String) RETURNS !Int64 -> @@ -114,6 +114,15 @@ FN run(program: String, input: String) RETURNS !String -> RETURN runWithLimits(program, input, 30_000, 10_000_000); END +FN runFile(path: String, input: String) RETURNS !String -> + RETURN runFileWithLimits(path, input, 30_000, 10_000_000); +END + +FN runFileWithLimits(path: String, input: String, tapeSize: Int64, maxSteps: Int64) RETURNS !String -> + program = readFile(path) OR RAISE; + RETURN runWithLimits(program, input, tapeSize, maxSteps); +END + FN runWithLimits(program: String, input: String, tapeSize: Int64, maxSteps: Int64) RETURNS !String -> code = commands(program); jumps = makeJumpTable(code); diff --git a/examples/brnfk/tests/392quine.b b/examples/brnfk/tests/392quine.b new file mode 100644 index 000000000..59860b4f8 --- /dev/null +++ b/examples/brnfk/tests/392quine.b @@ -0,0 +1,8 @@ +->++>+++>+>+>+++>>>>>>>>>>>>>>>>>>>>+>+>++>+++>++>>+++>+>>>>>>>>>>>>>>>>>>>>>>>> +>>>>>>>>>+>+>>+++>>+++>>>>>+++>+>>>>>>>>>++>+++>+++>+>>+++>>>+++>+>++>+++>>>+>+> +++>+++>+>+>>+++>>>>>>>+>+>>>+>+>++>+++>+++>+>>+++>>>+++>+>++>+++>++>>+>+>++>+++> ++>+>>+++>>>>>+++>+>>>>>++>+++>+++>+>>+++>>>+++>+>+++>+>>+++>>+++>>++[[>>+[>]++>+ ++[<]<-]>+[>]<+<+++[<]<+]>+[>]++++>++[[<++++++++++++++++>-]<+++++++++.<] +[Slight modification of Erik Bosman's ingenious 410-byte quine. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/400quine.b b/examples/brnfk/tests/400quine.b new file mode 100644 index 000000000..237a38708 --- /dev/null +++ b/examples/brnfk/tests/400quine.b @@ -0,0 +1,8 @@ +->++>+++>+>+>+++>>>>>>>>>>>>>>>>>>>>>>+>+>++>+++>++>>+++>+>>>>>>>>>>>>>>>>>>>>>> +>>>>>>>>>>>+>+>>+++>>>>+++>>>+++>+>>>>>>>++>+++>+++>+>+++>+>>+++>>>+++>+>++>+++> +>>+>+>+>+>++>+++>+>+>>+++>>>>>>>+>+>>>+>+>++>+++>+++>+>>+++>+++>+>+++>+>++>+++>+ ++>>+>+>++>+++>+>+>>+++>>>+++>+>>>++>+++>+++>+>>+++>>>+++>+>+++>+>>+++>>+++>>+[[> +>+[>]+>+[<]<-]>>[>]<+<+++[<]<<+]>+[>>]+++>+[+[<++++++++++++++++>-]<++++++++++.<] +[Slight modification of Erik Bosman's ingenious 410-byte quine. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/README.md b/examples/brnfk/tests/README.md new file mode 100644 index 000000000..7c535265b --- /dev/null +++ b/examples/brnfk/tests/README.md @@ -0,0 +1,15 @@ +Brainfuck.org corpus +==================== + +These `.b` programs were downloaded from https://www.brainfuck.org/. +The site attributes the programs to Daniel B. Cristofani and marks the +contents as Creative Commons Attribution-ShareAlike 4.0. + +`brnfk-corpus-tests.cht` runs a deterministic subset directly from these +files (`null.b`, `rot13.b`, `xmastree.b`, `squares.b`) and also encodes the +stable implementation-test snippets from `tests.b`. + +Some files in this corpus are intentionally interactive, open-ended, +stress-oriented, or produce non-printable bytes. They are vendored here as +reference programs, but they are not all part of the fast automated CLEAR +test. diff --git a/examples/brnfk/tests/bounce.b b/examples/brnfk/tests/bounce.b new file mode 100644 index 000000000..c6d76bb6e --- /dev/null +++ b/examples/brnfk/tests/bounce.b @@ -0,0 +1,172 @@ +[bounce.b -- particle automaton +(c) 2025 Daniel B. Cristofani +https://brainfuck.org/ +This program is licensed under a Creative Commons Attribution-ShareAlike 4.0 +International License (http://creativecommons.org/licenses/by-sa/4.0/).] + ++[ + ->>>+>>>>,----------[ + >++++[<----->-]+<--[ + +<++++[>---->++++<<-]>[ + >----[<->-]++<[ + >+<--[ + >++[>+++++<-]>[<<->++>-]<-<-[ + <++++[>->----<<-]>[ + >[-]+++++<--[ + <++++[>------>+<<-]>[ + ->[-]+< + ]]]]]]]] + ]>[<]<<<[<]< +]<[ + [-]<[ + [ + >[>>>>>>+<<<<<<-]<- + [[>]+<[-<]>]>>>>>[ + -<[>+<-]<[>+<-]<[>+<-]>>[<<+>>-]<<<[>>>+<<<-]>>>>[<<<<+>>>>-]>>>++<<< + ]>[ + -<<[>>+<<-]<[>+<-]<<[>>+<<-]>>>[<<<+>>>-]<<[>>+<<-]>>>>[<<<<+>>>>-]>>++++<< + ]>>[<++++++++>-]<+<<<[ + -<<<<<<<[<<<]>>>[<+[>>>+<<<-]>>>>] + <[>>>>>>>>[>>>]>+<<<<[<<<]<<<<<-] + >>>>>>>>[>>>]>>>[ + <<+[-[>>>+<<<-]>>[>>]<]<[++++++++<<]>>>[-]<<<<[<<<] + ]<<[-]<<<<[<<<] + ]<[ + -<<<<<<[<+[<<<+>>>-]<<]<<<[ + [>>[<<<+>>>-]<<<<<] + >>+[-[>>>+<<<-]>[>]>]<<[>++++<<]>>>[-]<<<<<[<<<] + ]>>[-]>>>>[>>>]>>>[>>>]>>> + ]<[>>>>>>>>[++<<+>]<[->]<<<<<<<-]<[<<<<[>+>]<[<<]>>>>>-]<<<< + ]<<< + ]>>>>>>>>>>>>>[ + [ + [<<<<<<<+>>>>>+>>-]++++[<++++++++>-]<<-[ + >[>+<-]>--[<++>-]<<-[ + >++<-[ + >[>+<--]>[<+++>-]<+<--[ + <++++[>->++++++<<-]>[ + >>++++++++[<<->--------->-]<+<[ + >+[<->>+++<---]>--[<++>-]<<[ + <++++[>---->-<<-]>[ + [-]>[<+>--]<--[>+<-]]]]]]]] + ]>.[-]>>>> + ]++++++++++.[-]>>> + ]<<<<<<<<<<<<, +] + +[This is a...particle automaton? You give it an initial configuration, +then you hit ENTER repeatedly to advance it one step each time. Holding +down ENTER may or may not be unsatisfyingly fast, depending on your +choice of pattern. + +Initial configurations can have particles (< > ^ or v to note direction +of movement), \ or / which are fixed walls for particles to bounce off, +X which is a combination of \ and / (and reverses movement by 180 as you +might expect), space which is empty space for particles to move through. + +Particles pass through each other, though in most cases the effect is +arguably the same as it should be if they had bounced off each other. + +When two particles, or one or more particles and a wall, are in the same +space, it's shown as *. This is not accepted as initial input, but it +would be pretty easy to write a replacement input routine that would +accept overlapping objects in an initial configuration, it'd just also +be somewhat harder to use. + +Particles that go off the edge of the configuration should be absorbed +neatly. This includes if they try to move upward or downward to a +shorter line that doesn't have enough trailing spaces explicitly typed. +After any particles have escaped that are going to, the behavior is +provably cyclical (and reversible). + +This is in brainfuck; it should run on any command-line interpreter or +any interpreter that does interactive i/o gracefully. + +A few sample patterns: + + X + /v\ + / \ + / \ + / \ + / \ +/ X +\ / + \ / + \ / + \ / + \ / + \/ +(All the interior spaces are part of one long path) + +/ > \ + / > \ + / > \ + / > \ + />\ + X + \ / + \ / + \ / + \ / +\ / +This should resynchronize every 480 steps. + + / > \ + / > \ + / >>>>>>> \ + / > \ +/ > > \ + + + + + + +\ / + \ / + \ / + \ / + \ / +Little demonstration of reflection. + + /\ + /\ \ + / \ \ + / /\ \ \ + \ \^ / / + \ \/ / + \ / + \/ + + +/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\ +X X +X ^^^vv>^>>v^^v> ^v ^^ v> v> v^>v^>^^^>v>^> X +X >> >^ <^^^v< v< v< ^< X +X >v <^ v^ v^^ ^v X +X << >^^<>> vv ^>^>>vv>^v ^> v<<^>^ ^< X +X >v ^vv ^<^^ v<^v <> vv ^v^^>^ ^> X +X >> ^^ ^><>>>>< >> >> X +X v>^^^^^<><<v <> <^ <^ ^^>^>^<^ X +X <^ <^>> ^v X +X >v ^>^<>>v><^>^><>^ ^^ >^<<>v >^v>v^ X +X v<>^ v^ <^^< ^< >vv^>>^^< X +X ^^>><< >^^<>^v< >><><>^^>^<<>v><v << v^ ^> v>^^>v>v> v> v^ ><>v ^^ X +X >v v^ X +X v >v<> <^^>^> >>><^^<>^>^ X +X ^< ^< >^vvv> <> ^v^<<> v^^^ ^v X +X v> v> ^< >^^<>< >^><<<<>v>< <<^< X +X <>^> ^>^<<> v^^<>><^><<<< v> <<^> ^< <> >v >^ X +X ^> ^v >>>v< ^> ^ vvvvv< ^< ^^>vv< >>>^^<<^< ^<>^ X +X v> ^vv<<< ^> ^^ >< ^^vv X +X ^v v>><>> v> vv<> >^ ^> <>v< >><>v>^<<< X +X v^ << >^ vv ^<>v v^v^v^ X +X >^v^vv^v^^<<<^ ^vv<>v >>vv ^^ < X +X X +\XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/ +] diff --git a/examples/brnfk/tests/bsort.b b/examples/brnfk/tests/bsort.b new file mode 100644 index 000000000..78a2eaad7 --- /dev/null +++ b/examples/brnfk/tests/bsort.b @@ -0,0 +1,14 @@ +[bsort.b -- bubble sort +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>,[>>,]<<[ +[<<]>>>>[ +<<[>+<<+>-] +>>[>+<<<<[->]>[<]>>-] +<<<[[-]>>[>+<-]>>[<<<+>>>-]] +>>[[<+>-]>>]< +]<<[>>+<<-]<< +]>>>>[.>>] + +[This program sorts the bytes of its input by bubble sort.] diff --git a/examples/brnfk/tests/chessboard.b b/examples/brnfk/tests/chessboard.b new file mode 100644 index 000000000..6af699086 --- /dev/null +++ b/examples/brnfk/tests/chessboard.b @@ -0,0 +1,97 @@ +[chessboard.b -- output ASCII art chessboard from FEN input +(c) 2025 Daniel B. Cristofani +http://brainfuck.org/ + +This program is licensed under a Creative Commons Attribution-ShareAlike 4.0 +International License (http://creativecommons.org/licenses/by-sa/4.0/). + +This program expects a chess position specified in Forysth-Edwards Notation. +It translates that into an ASCII art depiction of that board state, using +ASCII art chess pieces made by DMC (dmc75287@gmx.us) for a program (at +https://pastebin.com/u/bf17/1/kM751Hqz) that shows the initial board state. + +This one was fairly straightforward and also very difficult to get under 1024. +Many little tweaks, fiddly pointer movement. I'm sure this could be improved. + +We read and output one row at a time. Basic data layout is: +m f b | \ F / B x 0 l p f p f p f p f p f p f p f p f 0 0 0 .... +At left we have three flags. m is -1 if we still have more data to read +(i.e. the row we're now working with is not the last one). f is dual-purpose: +it may mean we don't need to flip the background color this time (at the very +start of each row), or it may mean the current piece is black (fill flag). +b means that the current background color is black. + +After that we have five semi-constant values used for ASCII output. | / and \ +are close to most values we need to output; F is for fill value (piece color) +and B is for background color. + +x is usually 0, but when we're about to do output it's where we put the +combined code for piece and line (1-4) within a piece which we'll use to +select what to output. l is the line counter (counting from 4 down to 1) +which also acts as a flag meaning we do have a row to output. + +Then the following batch of f and p are fill values (white=1, black=2) and +piece codes (1-7 for empty/bknpqr). + +I process input by first converting to binary with a binary counter, then +working with the bits. This is some of the fiddlier and dirtier part. + +The arrangement of outer loops was another wrinkle. I need to output the +horizontal row-separator bar at start and end; but the start one wants to be +AFTER reading the input, so the input doesn't interrupt the board. What I +ended up doing was, if m is still nonzero, then read input, and set l=4 as a +secondary flag meaning we need to output this input, and update m to say +whether that's the last row (based on whether we ended with a space or slash). +Then, unconditionally, we output the horizontal bar; and then check the l flag +and use it as a line counter if we have an actual row to output. So the last +time through the outer loop, it skips both main sub-loops (input and output) +and only outputs the bar. And skipping the output loop also means it doesn't +move back to a nonzero for the end of the main loop, so it terminates. + +Notice that my case statement feeds different fragments with identical ASCII +to the same case, to avoid duplication. Also note that the code for the top of +the pawn and for the bottom of all pieces need to consult flags internally.] + +->>++++>-->++>->--[++++[>+++++++++++++++<-]>++<<]>>++[ build constants + [<<]+<[ if need to read more + >[>>>]++++[ while more chars need reading this row + >>,[[>]+<[-<]>]>>>>>>>[ letter + -<+[<<<<<<+>>>>>>-]<[<<<++<+>>>>-]<[-]<[<+>-]<[<++>-]<[<<+>>-]> + ]<[ + -<[++++[<[<++>-]>-[<+>-]<]<[--[>+<-]+>]->] number + <+[-[[-<]<<[<]<->]+[-<+]>[>>]] row terminator + ]< + ] + ]>[>>]++++++++[<<<----.++.......++>>+>-]<<<----.++++>>++.[-]>> output bar + [if row to output + [while line to output + [>]<[>[>>+<<-]<<] + >>>>[for each space + <<<[<]<<<<<<.<<-[ if we need to flip background + +>[->>>>>++++[>+<--]] flip black to white + +>>[>>>---[>++<-]>>]<[<+>-]<[<] or white to black + ]>>[>]<.>>>[>]>>[<<+[<]<-[>>]>-] copy piece code + >-[-<<+[<]<<[<<]+>>>+++[>>]]<<+[<] set fill and fill flag + <+[ not an empty space + >>-[<+>-]+<[<++++++>>+<-]< combine line with piece code + [------[+++[+[+[----[--[-[-[+++++[------[-[-[-[-[-[-[-[[+] select + <<-------.<<<<+<[>->>...[>]]>[->>>+++...--->>>>]<<<+.++++++>>> 4bknpqr + ]<[.<.<..>>.>]> 1b (top of bishop; l value of 4) + ]<[.<<.>----.++++.>.>]> 1k + ]<[.<<+++.--->---.+++.<<<.[>]]> 1n + ]<[..<<<<<[[>]<.>]>[>>+++.--->>>>]<<..>]> 1p + ]<[.<<.<<.>>>.>.>]> 1q + ]<[<<-.++++.<<.>>.--.->>>]> 1r + ]<[.<<.<.>>.>.>]> 2b + ]]<[.<<<<-.>.<++.->>>>.>]> 23k + ]<[<.<+++.<.>>------.++++++<---.>>>]> 2n + ]]]]<[.<-------.<<.>>+.++++++>.>]> 2pq3bq + ]<[.<.<<.[>.>]]> 3p + ]<[.<.<<.>..>>>]> 3n + ]]<[.<<-.<.>++.->>.>]> 23r + ]<[.....>] empty + <.<<<<<<[>>>---<<<-]>>[>>]>[>]>> clear fill + ]<<<<[<<<]>>.[>>]++++++++++.[-]>- output | and linefeed + ]+[>]<[[-]<]< wipe line data + ]< +] diff --git a/examples/brnfk/tests/collatz.b b/examples/brnfk/tests/collatz.b new file mode 100644 index 000000000..c2dfe4a96 --- /dev/null +++ b/examples/brnfk/tests/collatz.b @@ -0,0 +1,33 @@ +>,[ + [ + ----------[ + >>>[>>>>]+[[-]+<[->>>>++>>>>+[>>>>]++[->+<<<<<]]<<<] + ++++++[>------<-]>--[>>[->>>>]+>+[<<<<]>-],< + ]> + ]>>>++>+>>[ + <<[>>>>[-]+++++++++<[>-<-]+++++++++>[-[<->-]+[<<<<]]<[>+<-]>] + >[>[>>>>]+[[-]<[+[->>>>]>+<]>[<+>[<<<<]]+<<<<]>>>[->>>>]+>+[<<<<]] + >[[>+>>[<<<<+>>>>-]>]<<<<[-]>[-<<<<]]>>>>>>> + ]>>+[[-]++++++>>>>]<<<<[[<++++++++>-]<.[-]<[-]<[-]<]<, +] + +[The Collatz problem or 3n+1 problem is as follows. Take a natural number n. +If it's even, halve it; if odd, triple it and add one. Repeat the process with +the resulting number, and continue indefinitely. If n is 0, the resulting +sequence is 0, 0, 0, 0... It is conjectured but not proven that for any +positive integer n, the resulting sequence will end in 1, 4, 2, 1... +See also http://www.research.att.com/projects/OEIS?Anum=A006577 + +This program takes a series of decimal numbers, followed by linefeeds (10). +The entire series is terminated by an EOF (0 or "no change"). For each number +input, the program outputs, in decimal, the number of steps from that number +to zero or one, when following the rule above. It's quite fast; on a Sun +machine, it took three seconds for a random 640-digit number. + +One more note. This program was originally written for Tristan Parker's +Brainfuck Texas Holdem contest, and won by default (it was the only entry); +the version I submitted before the contest deadline is at +http://www.hevanet.com/cristofd/brainfuck/oldcollatz.b + +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/dbf2c.b b/examples/brnfk/tests/dbf2c.b new file mode 100644 index 000000000..22aaf2f1e --- /dev/null +++ b/examples/brnfk/tests/dbf2c.b @@ -0,0 +1,35 @@ ++++[>+++++<-]>>+<[>>++++>++>+++++>+++++>+>>+<++[++<]>---] + +>++++.>>>.+++++.>------.<--.+++++++++.>+.+.<<<<---.[>]<<.<<<.-------.>++++. +<+++++.+.>-----.>+.<++++.>>++.>-----. + +<<<-----.+++++.-------.<--.<<<.>>>.<<+.>------.-..--.+++.-----<++.<--[>+<-] +>>>>>--.--.<++++.>>-.<<<.>>>--.>. + +<<<<-----.>----.++++++++.----<+.+++++++++>>--.+.++<<<<.[>]<.>> + +,[>>+++[<+++++++>-]<[<[-[-<]]>>[>]<-]<[<+++++>-[<+++>-[<-->-[<+++>- +[<++++[>[->>]<[>>]<<-]>[<+++>-[<--->-[<++++>-[<+++[>[-[-[-[->>]]]]<[>>]<<-] +>[<+>-[<->-[<++>-[<[-]>-]]]]]]]]]]]]] + +<[ + -[-[>+<-]>] + <[<<<<.>+++.+.+++.-------.>---.++.<.>-.++<<<<.[>]>>>>>>>>>] + <[[<]>++.--[>]>>>>>>>>] + <[<<++..-->>>>>>] + <[<<..>>>>>] + <[<<..-.+>>>>] + <[<<++..---.+>>>] + <[<<<.>>.>>>>>] + <[<<<<-----.+++++>.----.+++.+>---.<<<-.[>]>] + <[<<<<.-----.>++++.<++.+++>----.>---.<<<.-[>]] + <[<<<<<----.>>.<<.+++++.>>>+.++>.>>] + <.> +]> +,] + +<<<<<.<+.>++++.<----.>>---.<<<-.>>>+.>.>.[<]>++.[>]<. +>[Translates brainfuck to C. Assumes no-change-on-EOF or EOF->0. +Generated C does no-change-on-EOF, and uses unistd.h read and write calls. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/dbfi.b b/examples/brnfk/tests/dbfi.b new file mode 100644 index 000000000..a2df726d9 --- /dev/null +++ b/examples/brnfk/tests/dbfi.b @@ -0,0 +1,9 @@ +>>>+[[-]>>[-]++>+>+++++++[<++++>>++<-]++>>+>+>+++++[>++>++++++<<-]+>>>,<++[[>[ +->>]<[>>]<<-]<[<]<+>>[>]>[<+>-[[<+>-]>]<[[[-]<]++<-[<+++++++++>[<->-]>>]>>]]<< +]<]<[[<]>[[>]>>[>>]+[<<]<[<]<+>>-]>[>]+[->>]<<<<[[<<]<[<]+<<[+>+<<-[>-->+<<-[> ++<[>>+<<-]]]>[<+>-]<]++>>-->[>]>>[>>]]<<[>>+<[[<]<]>[[<<]<[<]+[-<+>>-[<<+>++>- +[<->[<<+>>-]]]<[>+<-]>]>[>]>]>[>>]>>]<<[>>+>>+>>]<<[->>>>>>>>]<<[>.>>>>>>>]<<[ +>->>>>>]<<[>,>>>]<<[>+>]<<[+<<]<] +[input a brainfuck program and its input, separated by an exclamation point. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/dquine.b b/examples/brnfk/tests/dquine.b new file mode 100644 index 000000000..12f1c5b4a --- /dev/null +++ b/examples/brnfk/tests/dquine.b @@ -0,0 +1,15 @@ +>+++++>+++>+++>+++++>+++>+++>+++++>++++++>+>++>+++>++++>++++>+++>+++>+++++>+>+ +>++++>+++++++>+>+++++>+>+>+++++>++++++>+++>+++>++>+>+>++++>++++++>++++>++++>+++ +>+++++>+++>+++>++++>++>+>+>+>+>++>++>++>+>+>++>+>+>++++++>++++++>+>+>++++++ +>++++++>+>+>+>+++++>++++++>+>+++++>+++>+++>++++>++>+>+>++>+>+>++>++>+>+>++>++>+ +>+>+>+>++>+>+>+>++++>++>++>+>+++++>++++++>+++>+++>+++>+++>+++>+++>++>+>+>+>+>++ +>+>+>++++>+++>+++>+++>+++++>+>+++++>++++++>+>+>+>++>+++>+++>+++++++>+++>++++>+ +>++>+>+++++++>++++++>+>+++++>++++++>+++>+++>++>++>++>++>++>++>+>++>++>++>++>++ +>++>++>++>++>+>++++>++>++>++>++>++>++>++>+++++>++++++>++++>+++>+++++>++++++>++++ +>+++>+++>++++>+>+>+>+>+++++>+++>+++++>++++++>+++>+++>+++>++>+>+>+>++++>++++ +[[>>>+<<<-]<]>>>>[<<[-]<[-]+++++++[>+++++++++>++++++<<-]>-.>+>[<.<<+>>>-]>] +<<<[>>+>>>>+<<<<<<-]>++[>>>+>>>>++>>++>>+>>+[<<]>-]>>>-->>-->>+>>+++>>>>+[<<] +<[[-[>>+<<-]>>]>.[>>]<<[[<+>-]<<]<<] +[Quine. Tidy (and portable, naturally) but not very short. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/dvorak.b b/examples/brnfk/tests/dvorak.b new file mode 100644 index 000000000..98de7bae5 --- /dev/null +++ b/examples/brnfk/tests/dvorak.b @@ -0,0 +1,16 @@ ++>>>>>>>++[+<[>]>[<++>-]<]<[[>+>+<<-]>>-]>+++++[>+++++++<-]>[[<<+>>-]<<-] +++++++[>++++++++++<-]>+<<<<<<<<<<++++++<<<<<<<+++++[<<+++>+>-]<++ +<[<<<<<<<+++++>>+++++>+>+++>>+++++>>+++++<-]<<<+<<--->--[[<<+>>-]<<-]>---<<<<- +<++++[<<<<++>->+++++++>+>-]<<[<<+>+>>+>>+>>++>>+<<<<<<<-]<[>+<-]<<- +>[[<<+>>-]<<-]<<<<++++++++++++[<<+>---->-]<<[[<<+>>-]<<-]+++[>---------<-] +<<<<<<<<<<<<<<<<<+<++++[<<++++>>-]<<[<<<--->>>>->>-->>>>>>---<<<<<<<<<-]<<<--<<[ + >>+>+++++++++++[<--->>>>---->>--->>--<<<<<<<-]>>>>>+>>+++ + >>>+++++++[<->>---->>->>--<<<<<-]>+>>---->>>>+++++>>---->>--> + ++++++[>--------<-]>+>>---->>+++>>------------>>>>++>>+++++++++>>-->>------ + >>---->>++>>+>+++++++[<++>>-<-]>>>+>>>+++++++[<+>>+++>>>>>>++++<<<<<<<-]>+ + >>>>>>>> +]>[<+>-]>[>>]<,[[[<<+>>-]<<-]>.[>>]<,] +[Filter for typing in Dvorak on a QWERTY keyboard. Needs a fast implementation. +Assumes no-change-on-EOF or EOF->0. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/e.b b/examples/brnfk/tests/e.b new file mode 100644 index 000000000..1d021e6e5 --- /dev/null +++ b/examples/brnfk/tests/e.b @@ -0,0 +1,40 @@ +[e.b -- compute e +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>>>++>+>++>+>>++<+[ + [>[>>[>>>>]<<<<[[>>>>+<<<<-]<<<<]>>>>>>]+<]>- + >>--[+[+++<<<<--]++>>>>--]+[>>>>]<<<<[<<+<+<]<<[ + >>>>>>[[<<<<+>>>>-]>>>>]<<<<<<<<[<<<<] + >>-[<<+>>-]+<<[->>>>[-[+>>>>-]-<<-[>>>>-]++>>+[-<<<<+]+>>>>]<<<<[<<<<]] + >[-[<+>-]]+<[->>>>[-[+>>>>-]-<<<-[>>>>-]++>>>+[-<<<<+]+>>>>]<<<<[<<<<]]<< + ]>>>+[>>>>]-[+<<<<--]++[<<<<]>>>+[ + >-[ + >>[--[++>>+>>--]-<[-[-[+++<<<<-]+>>>>-]]++>+[-<<<<+]++>>+>>] + <<[>[<-<<<]+<]>->>> + ]+>[>>>>]-[+<<<<--]++<[ + [>>>>]<<<<[ + -[+>[<->-]++<[[>-<-]++[<<<<]+>>+>>-]++<<<<-] + >-[+[<+[<<<<]>]<+>]+<[->->>>[-]]+<<<< + ] + ]>[<<<<]>[ + -[ + -[ + +++++[>++++++++<-]>-.>>>-[<<<----.<]<[<<]>>[-]>->>+[ + [>>>>]+[-[->>>>+>>>>>>>>-[-[+++<<<<[-]]+>>>>-]++[<<<<]]+<<<<]>>> + ]+<+<< + ]>[ + -[ + ->[--[++>>>>--]->[-[-[+++<<<<-]+>>>>-]]++<+[-<<<<+]++>>>>] + <<<<[>[<<<<]+<]>->> + ]< + ]>>>>[--[++>>>>--]-<--[+++>>>>--]+>+[-<<<<+]++>>>>]<<<<<[<<<<]< + ]>[>+<<++<]< + ]>[+>[--[++>>>>--]->--[+++>>>>--]+<+[-<<<<+]++>>>>]<<<[<<<<]]>> + ]> +] + +This program computes the transcendental number e, in decimal. Because this is +infinitely long, this program doesn't terminate on its own; you will have to +kill it. The fact that it doesn't output any linefeeds may also give certain +implementations trouble, including some of mine. diff --git a/examples/brnfk/tests/factorial.b b/examples/brnfk/tests/factorial.b new file mode 100644 index 000000000..06e3adf92 --- /dev/null +++ b/examples/brnfk/tests/factorial.b @@ -0,0 +1,7 @@ +>++++++++++>>>+>+[>>>+[-[<<<<<[+<<<<<]>>[[-]>[<<+>+>-]<[>+<-]<[>+<-[>+<-[> ++<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>[-]>>>>+>+<<<<<<-[>+<-]]]]]]]]]]]>[<+>- +]+>>>>>]<<<<<[<<<<<]>>>>>>>[>>>>>]++[-<<<<<]>>>>>>-]+>>>>>]<[>++<-]<<<<[<[ +>+<-]<<<<]>>[->[-]++++++[<++++++++>-]>>>>]<<<<<[<[>+>+<<-]>.<<<<<]>.>>>>] +This program doesn't terminate; you will have to kill it. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ diff --git a/examples/brnfk/tests/factorial2.b b/examples/brnfk/tests/factorial2.b new file mode 100644 index 000000000..e7a7b6ee7 --- /dev/null +++ b/examples/brnfk/tests/factorial2.b @@ -0,0 +1,24 @@ +[factorial2.b -- compute factorials +(c) 2019 Daniel B. Cristofani +http://brainfuck.org/] + +>>>>++>+[ + [ + >[>>]<[>+>]<<[>->>+<<<-]>+[ + [+>>[<<+>>-]>]+[-<<+<]>-[ + -[<+>>+<-]++++++[>++++++++<-]+>.[-]<<[ + >>>[[<<+>+>-]>>>]<<<<[[>+<-]<-<<]>- + ]>>>[ + <<-[<<+>>-]<+++++++++<[ + >[->+>]>>>[<<[<+>-]>>>+>>[-<]<[>]>+<]<<<<<<- + ]>[-]>+>>[<<<+>>>-]>>> + ]<<<+[-[+>>]<<<]>[<<<]> + ]>>>[<[>>>]<<<[[>>>+<<<-]<<<]>>>>>>>-[<]>>>[<<]<<[>+>]<]<< + ]++>> + ]<<++++++++.+ +] + +This program computes the factorials (https://oeis.org/A000142). Because this +sequence is infinitely long, this program doesn't terminate on its own; you will +have to kill it. This program is much faster than my earlier factorial program. + diff --git a/examples/brnfk/tests/fib.b b/examples/brnfk/tests/fib.b new file mode 100644 index 000000000..85bdace18 --- /dev/null +++ b/examples/brnfk/tests/fib.b @@ -0,0 +1,9 @@ +>++++++++++>+>+[ + [+++++[>++++++++<-]>.<++++++[>--------<-]+<<<]>.>>[ + [-]<[>+<-]>>[<<+>+>-]<[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- + [>+<-[>+<-[>+<-[>[-]>+>+<<<-[>+<-]]]]]]]]]]]+>>> + ]<<< +] +This program doesn't terminate; you will have to kill it. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ diff --git a/examples/brnfk/tests/ghost.b b/examples/brnfk/tests/ghost.b new file mode 100644 index 000000000..5714c0bcc --- /dev/null +++ b/examples/brnfk/tests/ghost.b @@ -0,0 +1,28 @@ +[ghost.b -- ghost game +(c) 2024 Daniel B. Cristofani +http://brainfuck.org/ +This program is licensed under a Creative Commons Attribution-ShareAlike 4.0 +International License (http://creativecommons.org/licenses/by-sa/4.0/).] + +[This is a reimplementation from scratch of June Bush's game Ghost Evade +(https://github.com/ABugNamedJune/GhostEvade/). +This version is more ruthless. Move your '@' with 'w' 'a' 's' or 'd', and a +linefeed.] + +-[ + ->+++++++++>+++>->>>>->>->>>>->>->>>>->+++ + >>->+>++>>+>->++>>+>->++>>+>->++>+++++++++ + >+>[>>>]<[>------->-------->->-->>]>>-[<-]< + ++[[[>>]+++<[>]++[<]<-]>-<<++]>+>[+>]<<[++++[>++++<-]<<] + <<<[[>->>>++++++++++<<<<+]<[>->>>>+<<<<<+]<]>>[>]>[<+>>+>>+<<<-] + >>>[-[>>+<<-]>>]<[<+>-]<[>++<-]<[<<]<<[>->>[>]+[<]<<-]->>>[>]<+[<-<]>+ + >[.>+++++++++[-[>>+<<-]>.>]++++++++++.[-]>]+[<<]<[ + ,->>>---[<<<--->>>-],<-<<[ + <++>>>++<<---[ + <++[>-----<-]>>+>-<<[ + >--<----[>+<[-]]]]]>[<+>>++++++++++<-] + >[-[>>+<<-]>>]++++[>--------<-]>[-[[<<]<[-]<[-]>>>>[-]]]+[>>]+[[-]<<]<<<<[ + [>>[<<->>-]>>+<<<<[+<<]>>>>]>>[<<]<<<<[+>>]>>>>[<<]>>[<<<<<<->>->>>>-]<<< + ]-[+<-]-< + ]> +]>>-----[>+++<-]>-.----------.++++++.>---.[<<+>-->----]<-.+++++.----.<++. diff --git a/examples/brnfk/tests/golden.b b/examples/brnfk/tests/golden.b new file mode 100644 index 000000000..08ea70cd1 --- /dev/null +++ b/examples/brnfk/tests/golden.b @@ -0,0 +1,19 @@ +[golden.b -- compute golden ratio +(c) 2019 Daniel B. Cristofani +http://brainfuck.org/] + ++>>>>>>>++>+>+>+>++<[ + +[ + --[++>>--]->--[ + +[ + +<+[-<<+]++<<[-[->-[>>-]++<[<<]++<<-]+<<]>>>>-<<<< + <++<-<<++++++[<++++++++>-]<.---<[->.[-]+++++>]>[[-]>>] + ]+>>-- + ]+<+[-<+<+]++>> + ]<<<<[[<<]>>[-[+++<<-]+>>-]++[<<]<<<<<+>] + >[->>[[>>>[>>]+[-[->>+>>>>-[-[+++<<[-]]+>>-]++[<<]]+<<]<-]<]]>>>>>>> +] + +This program computes the "golden ratio" (https://oeis.org/A001622). Because +this number is infinitely long, this program doesn't terminate on its own; +you will have to kill it. diff --git a/examples/brnfk/tests/head.b b/examples/brnfk/tests/head.b new file mode 100644 index 000000000..fcce4f810 --- /dev/null +++ b/examples/brnfk/tests/head.b @@ -0,0 +1,7 @@ +[head.b -- head (Unix utility) +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + ++>>>>>>>>>>-[,+[-.----------[[-]>]<->]<] + +[This program outputs the first ten lines of its input.] diff --git a/examples/brnfk/tests/impeccable.b b/examples/brnfk/tests/impeccable.b new file mode 100644 index 000000000..7a534ef67 --- /dev/null +++ b/examples/brnfk/tests/impeccable.b @@ -0,0 +1,18 @@ +[impeccable.b -- compute impeccable numbers +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>>->+[ + [ + [<<+>+>-]++++++[<<++++++++>>-]<<-.[-]< + ]++++++++++.[-]>>>++<[ + [-[[>>>]<<<-[+>>>]<<<[<<<]+>]<-[+>++++++++++>>]>]>>> + [[>+<-]>>>]<<[-[<++>-[<++>-[<++>-[<++>-[<[-]>-[<++>-]>>[<+<]>[->]<++<<]]]]]<+<<] + > + ]>[>>>]<<< +] + +This program outputs sequence (http://oeis.org/A014221). Although this sequence +is technically nonterminating, computing its eighth term would require more +storage than can exist in this universe, so you may as well kill this program +after the seventh term. diff --git a/examples/brnfk/tests/isort.b b/examples/brnfk/tests/isort.b new file mode 100644 index 000000000..804782af3 --- /dev/null +++ b/examples/brnfk/tests/isort.b @@ -0,0 +1,11 @@ +[isort.b -- insertion sort +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>+[ + <[ + [>>+<<-]>[<<+<[->>+[<]]>>>[>]<<-]<<< + ]>>[<<+>>-]<[>+<-]>[>>]<, +]<<<[<+<]>[>.>] + +[This program sorts bytes of input using insertion sort.] diff --git a/examples/brnfk/tests/jabh.b b/examples/brnfk/tests/jabh.b new file mode 100644 index 000000000..59d350343 --- /dev/null +++ b/examples/brnfk/tests/jabh.b @@ -0,0 +1,3 @@ ++++[>+++++<-]>[>+>+++>+>++>+++++>++<[++<]>---]>->-.[>++>+<<--]>--.--.+.> +>>++.<<.<------.+.+++++.>>-.<++++.<--.>>>.<<---.<.-->-.>+.[+++++.---<]>> +[.--->]<<.<+.++.++>+++[.<][http://www.hevanet.com/cristofd/brainfuck/]<. diff --git a/examples/brnfk/tests/life.b b/examples/brnfk/tests/life.b new file mode 100644 index 000000000..e068233be --- /dev/null +++ b/examples/brnfk/tests/life.b @@ -0,0 +1,38 @@ +[life.b -- John Horton Conway's Game of Life +(c) 2021 Daniel B. Cristofani +http://brainfuck.org/] + +>>>->+>+++++>(++++++++++)[[>>>+<<<-]>+++++>+>>+[<<+>>>>>+<<<-]<-]>>>>[ + [>>>+>+<<<<-]+++>>+[<+>>>+>+<<<-]>>[>[[>>>+<<<-]<]<<++>+>>>>>>-]<- +]+++>+>[[-]<+<[>+++++++++++++++++<-]<+]>>[ + [+++++++++.-------->>>]+[-<<<]>>>[>>,----------[>]<]<<[ + <<<[ + >--[<->>+>-<<-]<[[>>>]+>-[+>>+>-]+[<<<]<-]>++>[<+>-] + >[[>>>]+[<<<]>>>-]+[->>>]<-[++>]>[------<]>+++[<<<]> + ]< + ]>[ + -[+>>+>-]+>>+>>>+>[<<<]>->+>[ + >[->+>+++>>++[>>>]+++<<<++<<<++[>>>]>>>]<<<[>[>>>]+>>>] + <<<<<<<[<<++<+[-<<<+]->++>>>++>>>++<<<<]<<<+[-<<<+]+>->>->> + ]<<+<<+<<<+<<-[+<+<<-]+<+[ + ->+>[-<-<<[<<<]>[>>[>>>]<<+<[<<<]>-]] + <[<[<[<<<]>+>>[>>>]<<-]<[<<<]]>>>->>>[>>>]+> + ]>+[-<<[-]<]-[ + [>>>]<[<<[<<<]>>>>>+>[>>>]<-]>>>[>[>>>]<<<<+>[<<<]>>-]> + ]<<<<<<[---<-----[-[-[<->>+++<+++++++[-]]]]<+<+]> + ]>> +] + +[This program simulates the Game of Life cellular automaton. + +It duplicates the interface of the classic program at +http://www.linusakesson.net/programming/brainfuck/index.php, +but this program was written from scratch. + +Type e.g. "be" to toggle the fifth cell in the second row, "q" to quit, +or a bare linefeed to advance one generation. + +Grid wraps toroidally. Board size in parentheses in first line (2-166 work). + +This program is licensed under a Creative Commons Attribution-ShareAlike 4.0 +International License (http://creativecommons.org/licenses/by-sa/4.0/).] diff --git a/examples/brnfk/tests/null.b b/examples/brnfk/tests/null.b new file mode 100644 index 000000000..e69de29bb diff --git a/examples/brnfk/tests/numwarp.b b/examples/brnfk/tests/numwarp.b new file mode 100644 index 000000000..416d5593a --- /dev/null +++ b/examples/brnfk/tests/numwarp.b @@ -0,0 +1,34 @@ +>>>>+>+++>+++>>>>>+++[ + >,+>++++[>++++<-]>[<<[-[->]]>[<]>-]<<[ + >+>+>>+>+[<<<<]<+>>[+<]<[>]>+[[>>>]>>+[<<<<]>-]+<+>>>-[ + <<+[>]>>+<<<+<+<--------[ + <<-<<+[>]>+<<-<<-[ + <<<+<-[>>]<-<-<<<-<----[ + <<<->>>>+<-[ + <<<+[>]>+<<+<-<-[ + <<+<-<+[>>]<+<<<<+<-[ + <<-[>]>>-<<<-<-<-[ + <<<+<-[>>]<+<<<+<+<-[ + <<<<+[>]<-<<-[ + <<+[>]>>-<<<<-<-[ + >>>>>+<-<<<+<-[ + >>+<<-[ + <<-<-[>]>+<<-<-<-[ + <<+<+[>]<+<+<-[ + >>-<-<-[ + <<-[>]<+<++++[<-------->-]++<[ + <<+[>]>>-<-<<<<-[ + <<-<<->>>>-[ + <<<<+[>]>+<<<<-[ + <<+<<-[>>]<+<<<<<-[ + >>>>-<<<-<- + ]]]]]]]]]]]]]]]]]]]]]]>[>[[[<<<<]>+>>[>>>>>]<-]<]>>>+>>>>>>>+>]< +]<[-]<<<<<<<++<+++<+++[ + [>]>>>>>>++++++++[<<++++>++++++>-]<-<<[-[<+>>.<-]]<<<<[ + -[-[>+<-]>]>>>>>[.[>]]<<[<+>-]>>>[<<++[<+>--]>>-] + <<[->+<[<++>-]]<<<[<+>-]<<<< + ]>>+>>>--[<+>---]<.>>[[-]<<]< +] +[Enter a number using ()-./0123456789abcdef and space, and hit return. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/qsort.b b/examples/brnfk/tests/qsort.b new file mode 100644 index 000000000..232634d1b --- /dev/null +++ b/examples/brnfk/tests/qsort.b @@ -0,0 +1,9 @@ +[qsort.b -- quicksort +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>+>>>>>,[>+>>,]>+[--[+<<<-]<[<+>-]<[<[->[<<<+>>>>+<-]<<[>>+>[->]<<[<] +<-]>]>>>+<[[-]<[>+<-]<]>[[>>>]+<<<-<[<<[<<<]>>+>[>>>]<-]<<[<<<]>[>>[>> +>]<+<<[<<<]>-]]+<<<]+[->>>]>>]>>[.>>>] + +[This program sorts input bytes via quicksort.] diff --git a/examples/brnfk/tests/random.b b/examples/brnfk/tests/random.b new file mode 100644 index 000000000..6d222973f --- /dev/null +++ b/examples/brnfk/tests/random.b @@ -0,0 +1,14 @@ +>>>++[ + <++++++++[ + <[<++>-]>>[>>]+>>+[ + -[->>+<<<[<[<<]<+>]>[>[>>]]] + <[>>[-]]>[>[-<<]>[<+<]]+<< + ]<[>+<-]>>- + ]<.[-]>> +] +"Random" byte generator using the Rule 30 automaton. +Doesn't terminate; you will have to kill it. +To get x bytes you need 32x+4 cells. +Turn off any newline translation! +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ \ No newline at end of file diff --git a/examples/brnfk/tests/rot13.b b/examples/brnfk/tests/rot13.b new file mode 100644 index 000000000..c68e015bc --- /dev/null +++ b/examples/brnfk/tests/rot13.b @@ -0,0 +1,23 @@ +, +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>++++++++++++++<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>>+++++[<----->-]<<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>++++++++++++++<- +[>+<-[>+<-[>+<-[>+<-[>+<- +[>++++++++++++++<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>>+++++[<----->-]<<- +[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-[>+<- +[>++++++++++++++<- +[>+<-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] +]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]>.[-]<,] + +of course any function char f(char) can be made easily on the same principle + +[Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/short.b b/examples/brnfk/tests/short.b new file mode 100644 index 000000000..088f104fc --- /dev/null +++ b/examples/brnfk/tests/short.b @@ -0,0 +1,75 @@ +Although brainfuck is basically useless, it isn't quite so crashingly useless +as people usually say. Some small tasks can be done fairly concisely. +(Some of these are fairly UNIX-specific.) + +Do nothing, terminate successfully. +(Also called "true".) +(This is the shortest brainfuck quine as well.) + ++[>.+<] +Real random byte generator. +(Send output to file, stop program after a while, and take the last byte.) + ++++++++. +Beep. +(Small binary data files are easy to make with brainfuck and a pipe, too.) + +,[.[-],] +Copy input to output. With a pipe, a no-frills way to make text files. +(Unlike most programs assuming EOF->0, this needed modification before it would +work with EOF->"no change" as well.) +(remove the . to get an input-devourer.) + +>,[>,]<[.<] +Reverse input. + +unmatched [. +A Kimian quine, for my Sun compiler. + +,[---------[-[++++++++++.[-]]],] +Strip tabs and linefeeds. + +++++[>++++++++<-],[[>+.-<-]>.<,] +Show ASCII values of input in unary, separated by spaces. +(Useful for checking your implementation's newline behavior on input.) + +++++++++++[>++++++++++>+<<-]>[>.<-] +Clear the screen. + ++++++++++++[>++++++>+<<-]>+.>-[.[-],] +Play "Prisoner's Dilemma" using the robust "Tit for Tat" strategy. + ++++++++++++[>++++++>+<<-]>++.>-.>,[,[-]<<.>.>,] +...and using the not-so-robust "All D" strategy. + ++++++[>+++++++++<-],[[>--.++>+<<-]>+.->[<.>-]<<,] +Translate text to brainfuck that prints it. + +>>,[>>,]<<[[-<+<]>[>[>>]<[.[-]<[[>>+<<-]<]>>]>]<<] +Sort bytes in ascending order. +(These last two are solutions from Brainfuck Golf.) + +>,[>>>++++++++[<[<++>-]<+[>+<-]<-[-[-<]>]>[-<]<,>>>-]<.[-]<<] +Translate "binary" to ASCII, e.g. "0110100001101001" to "hi". + ++++++[>+++++<-]>[>++>++++<<-]>-->-<[[>[[>>+<<-]<]>>>-]>-[>+>+<<-]>] +A novel way of moving to the 9999th cell. + +>++++[>++++++<-]>-[[<+++++>>+<-]>-]<<[<]>>>>- +-.<<<-.>>>-.<.<.>---.<<+++.>>>++.<<---.[>]<<. +Print "brainfuck" with a linefeed. + +>>++>+<[[>>]+>>+[-[++++++[>+++++++>+<<-]>-.>[< +------>-]++<<]<[>>[-]]>[>[-<<]+<[<+<]]+<<]>>] +Version of random.b that outputs printable ASCII: "11011100..." + +++++++++[>++++[>++>+++>+++>+<<<<-]>+>->+>>+[<]<-]>>.> +>---.+++++++..+++.>.<<-.>.+++.------.--------.>+.>++. +Print "Hello World!" with a linefeed. + +>++++++++++>>+<+[[+++++[>++++++++<-]>.<++++++[>--------<-]+<<]>.>[->[ +<++>-[<++>-[<++>-[<++>-[<-------->>[-]++<-[<++>-]]]]]]<[>+<-]+>>]<<] +Output powers of two, in decimal. (Doesn't terminate.) + +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ diff --git a/examples/brnfk/tests/sierpinski.b b/examples/brnfk/tests/sierpinski.b new file mode 100644 index 000000000..71f2abfdd --- /dev/null +++ b/examples/brnfk/tests/sierpinski.b @@ -0,0 +1,12 @@ +[sierpinski.b -- display Sierpinski triangle +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ + -<<<[ + ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< + ]>.>+[>>]>+ +] + +[Shows an ASCII representation of the Sierpinski triangle +(iteration 5).] diff --git a/examples/brnfk/tests/squares.b b/examples/brnfk/tests/squares.b new file mode 100644 index 000000000..b2354859b --- /dev/null +++ b/examples/brnfk/tests/squares.b @@ -0,0 +1,8 @@ +++++[>+++++<-]>[<+++++>-]+<+[ + >[>+>+<<-]++>>[<<+>>-]>>>[-]++>[-]+ + >>>+[[-]++++++>>>]<<<[[<++++++++<++>>-]+<.<[>----<-]<] + <<[>>>>>[>>>[-]+++++++++<[>-<-]+++++++++>[-[<->-]+[<<<]]<[>+<-]>]<<-]<<- +] +[Outputs square numbers from 0 to 10000. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/squares2.b b/examples/brnfk/tests/squares2.b new file mode 100644 index 000000000..e18e5d75d --- /dev/null +++ b/examples/brnfk/tests/squares2.b @@ -0,0 +1,15 @@ +[squares2.b -- compute square numbers +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>>>>>>>>>+>++<[ + [[<<+>+>-]++++++[<++++++++>-]<-.[-]<<<] + ++++++++++.[-]>>>>>[>>>>]<<<<[[<<<+>+>>-]<<<-<]>>++[ + [ + <<<++++++++++[>>>[->>+<]>[<]<<<<-] + >>>[>>[-]>>+<<<<[>>+<<-]]>>>> + ]<<-[+>>>>]+[<<<<]> + ]>>>[>>>>]<<<<-<<+<< +] + +This program outputs square numbers. It doesn't terminate. diff --git a/examples/brnfk/tests/tests.b b/examples/brnfk/tests/tests.b new file mode 100644 index 000000000..bec2b9249 --- /dev/null +++ b/examples/brnfk/tests/tests.b @@ -0,0 +1,68 @@ +Here are some little programs for testing brainfuck implementations. + + +>,>+++++++++,>+++++++++++[<++++++<++++++<+>>>-]<<.>.<<-.>.>.<<. +This is for testing i/o; give it a return followed by an EOF. (Try it both +with file input--a file consisting only of one blank line--and with +keyboard input, i.e. hit return and then ctrl-d (Unix) or ctrl-z +(Windows).) +It should give two lines of output; the two lines should be identical, and +should be lined up one over the other. If that doesn't happen, ten is not +coming through as newline on output. +The content of the lines tells how input is being processed; each line +should be two uppercase letters. +Anything with O in it means newline is not coming through as ten on input. +LK means newline input is working fine, and EOF leaves the cell unchanged +(which I recommend). +LB means newline input is working fine, and EOF translates as 0. +LA means newline input is working fine, and EOF translates as -1. +Anything else is fairly unexpected. + + +++++[>++++++<-]>[>+++++>+++++++<<-]>>++++<[[>[[>>+<<-]<]>>>-]>-[>+>+<<-]>] ++++++[>+++++++<<++>-]>.<<. +Goes to cell 30000 and reports from there with a #. (Verifies that the +array is big enough.) + + +These next two test the array bounds checking. Bounds checking is not +essential, and in a high-level implementation it is likely to introduce +extra overhead. In a low-level implementation you can get bounds checking +for free by using the OS's own memory protections; this is the best +solution, which may require making the array size a multiple of the page +size. +Anyway. These two programs measure the "real" size of the array, in some +sense, in cells left and right of the initial cell respectively. They +output the result in unary; the easiest thing is to direct them to a file +and measure its size, or (on Unix) pipe the output to wc. If bounds +checking is present and working, the left should measure 0 and the right +should be the array size minus one. ++[<+++++++++++++++++++++++++++++++++.] + ++[>+++++++++++++++++++++++++++++++++.] + + +[]++++++++++[>>+>+>++++++[<<+<+++>>>-]<<<<-] +"A*$";?@![#>>+<<]>[>>]<<<<[>++<[-]]>.>. +Tests for several obscure problems. Should output an H. + + ++++++[>+++++++>++<<-]>.>.[ +Should ideally give error message "unmatched [" or the like, and not give +any output. Not essential. + ++++++[>+++++++>++<<-]>.>.][ +Should ideally give error message "unmatched ]" or the like, and not give +any output. Not essential. + + +My pathological program rot13.b is good for testing the response to deep +brackets; the input "~mlk zyx" should produce the output "~zyx mlk". + + +For an overall stress test, and also to check whether the output is +monospaced as it ideally should be, I would run numwarp.b. + + +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ diff --git a/examples/brnfk/tests/thuemorse.b b/examples/brnfk/tests/thuemorse.b new file mode 100644 index 000000000..e0f26ab7d --- /dev/null +++ b/examples/brnfk/tests/thuemorse.b @@ -0,0 +1,5 @@ +>>++++++[>++++++++<-]+[[>.[>]+<<[->-<<<]>[>+<<]>]>++<++] +Thue-Morse sequence (http://www.research.att.com/projects/OEIS?Anum=A010060). +Takes O(n) time and O(lg n) space (which are optimal); does not terminate. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ diff --git a/examples/brnfk/tests/tictactoe.b b/examples/brnfk/tests/tictactoe.b new file mode 100644 index 000000000..cb556625f --- /dev/null +++ b/examples/brnfk/tests/tictactoe.b @@ -0,0 +1,52 @@ +[tictactoe.b -- play tic-tac-toe +(c) 2020 Daniel B. Cristofani +http://brainfuck.org/ +This program is licensed under a Creative Commons Attribution-ShareAlike 4.0 +International License (http://creativecommons.org/licenses/by-sa/4.0/).] + +--->--->>>>->->->>>>>-->>>>>>>>>>>>>>>>>>+>>++++++++++[ + <<++[ + --<+<<+<<+>>>>[ + >[<->>+++>>[-]+++<<<+[<++>>+<--]]+>+++++[>>+++++++++<<-] + >>++++.[-]>>+[<<<<+>>+>>-]<<<<<<[>+<-]<< + ]++++++++++.[-]>++ + ]-->>[-->[-]>]<<[ + >>--[ + -[ + -[ + -----[>+>+++++++<<+]-->>-.----->,[<->-]<[[<]+[->>]<-]<[<<,[-]]>>>> + ]> + ]<[ + >-[+<+++]+<+++[+[---->]+<<<<<<[>>]<[-]] + >[<+[---->]++[<]<[>]>[[>]+>+++++++++<<-[<]]]>[>>>>] + ]<[ + -[[>+>+<<-]>[<+>-]++>+>>]<[<<++[-->>[-]]>[[-]>[<<+>>-]>]] + ]<[ + [[<<]-[>>]<+<-]>[-<+]<<[<<]-<[>[+>>]>[>]>[-]] + >[[+>>]<-->>[>]+>>>] + ]<[ + -[ + --[+<<<<--[+>[-]>[<<+>+>-]<<[>>+<<-]]++[>]] + <<[>+>+<<-]>--[<+>-]++>>> + ]<[<<<[-]+++>[-]>[<+>>>+<<-]+>>>] + ]<[ + +[[<]<<[<<]-<->>+>[>>]>[>]<-]+[-<+]<++[[>+<-]++<[<<->>+]<++]< + <<<<<< +> > >+> > >+[ + <<< ->+>+>+[ + <<<<<<< +>->+> > >->->+[ + <<<<< ->+>+> >+>+[ + <<<< ->->+>->+[ + <<<<<<<<+>-> >+> > >->+>+[ + <<<<< -> >+> >->+[ + <<<< +>->+> >+]]]]]]] + +++[[>+<-]<+++]--->>[-[<->-]<++>>]++[[<->-]>>]>[>] + ]< + ] + ]< +] + +[This program plays tic-tac-toe. I've given it the first move. It needs +interactive i/o, e.g. a command-line brainfuck interpreter or a brainfuck +compiler that produces command-line executables. At the '>' prompt, enter +the number of an empty space, followed by a linefeed, to play a move there.] + diff --git a/examples/brnfk/tests/utm.b b/examples/brnfk/tests/utm.b new file mode 100644 index 000000000..4540f73bb --- /dev/null +++ b/examples/brnfk/tests/utm.b @@ -0,0 +1,491 @@ +[A universal Turing machine from Yurii Rogozhin's article "Small universal +Turing machines", in Theoretical Computer Science, 168(2):215-240, 20 November +1996. Thus, a very direct proof that brainfuck is Turing-complete. For i/o +formats and so on, read below; for fuller detail, dig up the article. + +If you just want a quick and complete test case, the input b1b1bbb1c1c11111d +should produce the output 1c11111. + +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/ + + + +This Turing machine achieves Turing-completeness not by simulating other +Turing machines directly, but by simulating a Turing-complete class of +tag-systems (a computational model invented by Emil Post and named after the +children's game "tag"). A tag-system transforms strings over an alphabet A = +{a[1], a[2], ... a[n], a[n+1]} as follows: a positive integer m is chosen, and +so is a function P that maps each a[i] for 1<=i<=n to a string P(a[i]) over +the alphabet A. Now: + +1. if the string being transformed has fewer than m elements, the whole +process stops now. +2. m elements are removed from the beginning of the string +3. call the first element removed a[k]; if k=n+1 the whole process stops now. +4. P(a[k]) is appended to the string. +5. steps 1-5 are repeated. + +The particular class of tag-systems this Turing machine simulates is the class +where m=2, the initial string has length at least 2, and all P(a[i]) where +1<=i<=n are of the form a[n]a[n]B[i] where B[i] is some string over the +alphabet A (B[i] is the empty string if and only if i=n). + +The input for this brainfuck program is mildly complex, and there is no error +checking. The complexity comes from the encoding of tag-systems in terms of +Turing machine tape configurations. Note that the set of initial tape +configurations that represent tag-systems from the above class is a small, +though Turing-complete, subset of the set of possible initial tape +configurations for this Turing machine; and the following brainfuck program is +only designed to accept inputs from that subset. + +-The representation of a symbol a[i] from the alphabet A is a string of 1s +which is one element longer than twice the combined length of all P(a[j]) +where 1<=jR1 2b>L3 3bL2 3< H 4< +1>bL1 2>bR3 4>" and each "1" +unchanged); they are followed by a series of "1" cells, then a "c" (the +leftmost one at that time), then the cells representing the final state of the +transformed string, then a "c" and a sequence of "1" cells representing a[n+1] +as mentioned. + +The minimal test case b1b1bbb1c1c11111 represents the tag-system where P(a[1]) += a[1]a[1] and P(a[2]) = STOP, applied to the string a[1]a[1]a[2]. This runs +for 518 steps of the Turing machine, exercising all 23 Turing machine +instructions, before halting with the output string a[1]. + + +Here is the brainfuck program that implements this Turing machine. The basic +memory layout is as follows. +Each Turing machine cell is represented by a brainfuck cell, with the symbols +"0 1 b < > c" represented by 0, 1, 2, 3, 4, 5 respectively. The brainfuck +cells representing the Turing machine cells are laid out contiguously from the +beginning of the tape, except that the head of the Turing machine is +represented by a gap of three brainfuck cells, just to the left of the +brainfuck cell that represents the current Turing machine cell. At the start +of each cycle, the rightmost of these three cells holds the Turing machine +state, where states 1-4 are represented by 1-4 and "halt" (here treated as a +separate state) is represented by 0. The other two cells hold zeroes. + +Now to walk through the code: + ++++>++>>>+[ + +Set up 3 2 0 0 1, representing "< b" and the Turing machine head, in the +initial state 1; we can put this at the left end of the brainfuck array +because the Turing machine will never go left from the "<". +Next, start the main input-processing loop. Each time through this loop, we +begin at the rightmost tape cell that we have filled so far, or at the state +cell of the Turing machine head if it is to the right of all tape cells (as it +is initially). Each time through, we read a character; if it is "1" or "c", we +add the appropriate code to the right end of the tape; if it is a "b", we not +only add the code to the end of the tape but also move the head to the right +of it, since the head must follow the rightmost "b" when the Turing machine +starts; if the input character is a linefeed or other terminator, we add +nothing to the tape but position the brainfuck pointer at the zero that +follows the last filled tape cell, thus ending this loop. + +>>,[>+++++<[[->]<<]<[>]>] + +Read input, producing +... x 0 'i 0 ... +where "x" is a nonzero cell and "i" is the input. +While i is nonzero, run this loop: +-set up +... x 0 'i 5 0 ... +-If the input was six or greater, the [[->]<<] part will five times decrement +both i and 5; the sixth time, it will only decrement i, and move to the cell +left of i, producing +... x '0 i 0 0 ... +following which the code <[>]> will restore the pointer to i: +... x 0 'i 0 0 ... +In short, while i is at least six, the net effect of each iteration of the +loop [>+++++<[[->]<<]<[>]>] is to reduce i by 6; so repeated iterations will +change i to i mod 6; call this j. Then the loop will be run once more. +Now legitimate input characters give the values 1, 2, 3, 4 when reduced mod 6; +"1" gives 1, "b" gives 2, "c" gives 3, and linefeed and "." and "d" all give +4. On the last run through the loop [>+++++<[[->]<<]<[>]>] , the [[->]<<] part +will decrement both j and 5 repeatedly until j is zeroed, i.e. it will zero j +while reducing 5 by j, leaving +... x 0 '0 r 0 ... +where r is 5-j. The code <[>]> leaves this configuration unchanged, and the +loop exits. + +>-[<<+++++>>-[<<---->>-[->]<]] + +If r was 1, i mod 6 was 4, meaning a terminator. So we don't fill any tape +cell but leave +... x 0 0 '0 ... +If r was 2, i mod 6 was 3, meaning "c". So we set up +... x 5 0 '0 ... +If r was 3, i mod 6 was 2, meaning "b". In this case we set up +... x 1 0 '0 ... +and skip the innermost [->] loop, then step left leaving +... x 1 '0 0 ... +(note the pointer position; only in this case is the pointer immediately to +the right of a nonzero cell.) +If r was 4, i mod 6 was 1, meaning "1". In this case we set up +... x 1 0 '1 0 ... +and enter the inner [->] loop, resulting in +... x 1 0 0 '0 ... +after which we step left, producing +... x 1 0 '0 ... + +<[<-<[<]+<+[>]<<+>->>>] + +Now we step left. If and only if i was "b", we will enter this loop which will +move the Turing machine head. Now the situation, including the head which is +somewhere off to the left, is +... 2 0 0 1 y ... y y y y '1 0 0 0 ... +where each y is a 1 (since that is the only symbol that occurs between one b +and the next b, in input strings that represent tag-systems). +Now we zero one y and scan left: +... 2 0 '0 1 y ... y y y 0 1 0 0 0 ... +set two more 1's, and scan right (these two, and the 1 that was formerly the +state cell of the head, now serve as tape symbols, so we label them y); +... 2 y y y y ... y y y '0 1 0 0 0 ... +next, we set up the head at its new position, and we set the cell to the left +of it to b, and move the pointer just right of the 1 which is the state: +... 2 y y y y ... y 2 0 0 1 '0 0 0 ... +and now we're to the right of the rightmost nonzero cell, as we should be, so +we end the head-moving loop. + +(In the degenerate case where we received two b's in a row, the process goes: +...2 0 0 1 '1 0 0 0 ... +...2 0 '0 0 1 0 0 0 ... +...2 y y '0 1 0 0 0 ... +...2 2 0 0 1 '0 0 0 ... +as it should.) + +After either performing or skipping that loop, there are four possibilities +corresponding to the four possible inputs. +... x x x x 1 '0 ... if the input was a "1" +... x 2 0 0 1 '0 ... if the input was a "b" +... x x x x 5 '0 ... if the input was a "c" +... x x x x 0 '0 ... if the input was a terminator. + +<]<[<]>[ + +Now we move left and close the loop. If the input was anything but a +terminator, this puts us at the rightmost nonzero cell and we repeat the +input-processing loop. If the input was a terminator, this puts us at the zero +after the rightmost nonzero cell, and the input is already finished; then we +scan left to the gap that represents the Turing machine head, and position the +pointer at the state cell. Then we begin the main loop, which will be executed +once for each step of the Turing machine's operation, stopping when the state +cell holds 0 (representing the halt state). + +-[>++++++<-]>[<+>-] + +At the beginning of each iteration of the main processing loop, the +configuration is +... w x 0 0 's y z ... +where w, x, y, z are tape cells, with y being the current tape cell, and s is +the state (1-4). First we combine the current state with the current symbol; +we add (6*(s-1)) onto y, then move the result back into the former location of +s. Call the combination g; its values range from 0 to 23, one for each +state-symbol combination. +... w x 0 0 '0 g z ... +... w x 0 0 g '0 z ... + +Now we have to use g to select a new symbol, a new state, and a direction to +move the head. We will provisionally move the head right one cell, and set a +direction flag; if that flag is nonzero, we will move the head left two cells, +resulting in a total movement of one cell to the left. That is, we want to use +g in +... w x 0 0 g 0 z ... +to construct an appropriate +... w x y d 0 s z ... +where d is the direction flag, s is the new state, and y is the new symbol; +then if d is nonzero ("move left after all") we want to shift this to produce +... w 0 0 s x y z ... + +The way we use g to pick new values for s, y, and d is a very general scheme +for mathematical functions of one variable, and this UTM is one place we need +the generality. (See my rot13.b for a place where I didn't need the +generality, and used this method anyway, leading to comically inconcise but +fast code.) The basic idea is like +>set f(0)<[>set f(1)<-[>set f(2)<-[>set f(3)<-[...&c...]]]] + +The argument (in this case, g) is gradually decremented; if it was (say) five, +the program will enter the five outermost loops of this nest but will skip the +sixth and those inside it, since by that point the argument will have been +zeroed. So we just make sure the output cells have the right values for f(5) +at that point, by setting them inside the fifth loop but outside the sixth. + + +We get the outputs from the state table, naturally, reading down the columns. +Recall that "01b<>c" map to 0 1 2 3 4 5, and that "d=1" means "left". + ++<<<+++>+> +g>=0; set s=1, y=3, d=1 + +[- +g>=1; same values. There's some continuity in the table; only changes from one +combination to the next will be commented hereafter. + +[<<+>->- +g>=2; y=4, d=0 + +[<<[-]>>- +g>=3; y=0 + +[<<++>+>- +g>=4; y=2, d=1 + +[<<-->->>+++<- +g>=5; y=0, d=0, s=4 + +[<<+>+>>--<- +g>=6; y=1, d=1, s=2 + +[<<->->- +g>=7; y=0, d=0 + +[<<++++>+>>+<- +g>=8; y=4, d=1, s=3 + +[>-<- +g>=9; s=2 + +[<<->->- +g>=10; y=3, d=0 + +[<<->>- +g>=11; y=2 + +[<<+++>>>-<- +g>=12; y=5, s=1 + +[<<---->>>++<- +g>=13; y=1, s=3 + +[<<++>>>+<- +g>=14; y=3, s=4 + +[>[-]<- +g>=15; s=0 (this is the halt condition; having it produce d=0 is useful, since +moving left would take us outside the brainfuck array, and the capability of +actually not moving the head has been omitted as unnecessary, given that we're +only going to output the part of the tape that holds the final state of the +transformed string.) + +[<<->>>+++<- +g>=16; y=2, s=3 + +[<<->>>--<- +g>=17; y=1, s=1 + +[<<++++>+>>+<- +g>=18; y=5, d=1, s=2 + +[<<[-]>->>++<- +g>=19; y=0, d=0, s=4 + +[<<+++++>+>>--<- +g>=20; y=5, d=1, s=2 + +[<->>++<[<<->>-]] +Here's a tricky part. g==21 is never produced by the Turing machine from +correct input, so the remaining states to consider are g==22 and g==23, which +should give (y=3, d=0, s=4) and (y=2, d=0, s=4) respectively. So we set up +d=0, s=4 which are common to both, then we take the 2 or 3 that remains in g +and subtract it from y to produce the right result for y. + +]]]]]]]]]]]]]]]]]]]] + +We arrive here when g has been zeroed. Again, the layout now is +... w x y d '0 s z ... +and y, d, and s have the correct values. + +<[->>[<<+>>-]<<<[>>>+<<<-]<[>>>+<<<-]] + +If d==1 we do: +... w x y '0 0 s z ... +... w x y s 0 '0 z ... +... w x '0 s 0 y z ... +... w '0 0 s x y z ... +to move the head left and leave the pointer at the leftmost cell of the head, +where it would be if d had been 0 also. + +>>] + +Go to the state cell, and if it is nonzero (not the halt state) go through the +main Turing machine loop again. + +>[-[---[-<]]>] + +Now the Turing machine has halted. The situation is +4 0 0 '0 x x ... x x 0 0 0 ... +where each x is either 1, 4, or 5, and the leftmost 5 ("c") marks the start of +the transformed string. So we scan through the tape looking for that 5, and +incidentally clearing the tape as we go. When we find a 1, we decrement it, +skip the loop [---[-<]], move right, and start fresh. When we find a 4, we +decrement it down to 0, skip the loop [-<], move right, and start fresh. When +we find a 5, we decrement it down to 0, move left, move right to the space the +5 occupied (now 0), and end this loop. + +>[+++[<+++++>--]>] + +Now we want to output the part of the tape that represents the transformed +string; the situation is +... 0 '0 x x x ... x x x 0 0 0 ... +where each x is either 1, representing "1", or 5, representing "c". To +transform each to the right ASCII value we first add 3, producing 4 for "1" +and 8 for "c", then multiply the resulting value by 5/2 while moving it left; +this is safe because we know the value is even. We scan through the string +this way, after which it consists of 10s (for "1") and 20s (for "c"), and the +whole string has been shifted one cell left. + ++<++[[>+++++<-]<] + +Now we set two more cells to make the linefeed, producing +... 0 x x x ... x x x '2 1 0 0 ... +and multiply each cell by five while moving it right, producing +... '0 0 x x x ... x x x 11 0 0 ... +where each x is either 50 (for "1") or 100 (for "c"). + +>>[-.>] + +Now we scan right, reducing each cell by one and outputting it; the values are +49 (ASCII for "1"), 99 (ASCII for "c"), and 10 (ASCII for the final linefeed). +After this loop there are no more commands, so the program terminates.] + + +The entire program again without comments: + ++++>++>>>+[>>,[>+++++<[[->]<<]<[>]>]>-[<<+++++>>-[<<---->>-[->]<]] +<[<-<[<]+<+[>]<<+>->>>]<]<[<]>[-[>++++++<-]>[<+>-]+<<<+++>+> + [- + [<<+>->- + [<<[-]>>- + [<<++>+>- + [<<-->->>+++<- + [<<+>+>>--<- + [<<->->- + [<<++++>+>>+<- + [>-<- + [<<->->- + [<<->>- + [<<+++>>>-<- + [<<---->>>++<- + [<<++>>>+<- + [>[-]<- + [<<->>>+++<- + [<<->>>--<- + [<<++++>+>>+<- + [<<[-]>->>++<- + [<<+++++>+>>--<- + [<->>++< + [<<->>- +]]]]]]]]]]]]]]]]]]]]]]<[->>[<<+>>-]<<<[>>>+<<<-]<[>>>+<<<-]]>>] +>[-[---[-<]]>]>[+++[<+++++>--]>]+<++[[>+++++<-]<]>>[-.>] + + diff --git a/examples/brnfk/tests/wc.b b/examples/brnfk/tests/wc.b new file mode 100644 index 000000000..28d998483 --- /dev/null +++ b/examples/brnfk/tests/wc.b @@ -0,0 +1,13 @@ +>>>+>>>>>+>>+>>+[<<],[ + -[-[-[-[-[-[-[-[<+>-[>+<-[>-<-[-[-[<++[<++++++>-]< + [>>[-<]<[>]<-]>>[<+>-[<->[-]]]]]]]]]]]]]]]] + <[-<<[-]+>]<<[>>>>>>+<<<<<<-]>[>]>>>>>>>+>[ + <+[ + >+++++++++<-[>-<-]++>[<+++++++>-[<->-]+[+>>>>>>]] + <[>+<-]>[>>>>>++>[-]]+< + ]>[-<<<<<<]>>>> + ], +]+<++>>>[[+++++>>>>>>]<+>+[[<++++++++>-]<.<<<<<]>>>>>>>>] +[Counts lines, words, bytes. Assumes no-change-on-EOF or EOF->0. +Daniel B Cristofani (cristofdathevanetdotcom) +http://www.hevanet.com/cristofd/brainfuck/] diff --git a/examples/brnfk/tests/xmastree.b b/examples/brnfk/tests/xmastree.b new file mode 100644 index 000000000..8ff326093 --- /dev/null +++ b/examples/brnfk/tests/xmastree.b @@ -0,0 +1,8 @@ +[xmastree.b -- print Christmas tree +(c) 2016 Daniel B. Cristofani +http://brainfuck.org/] + +>>>--------<,[<[>++++++++++<-]>>[<------>>-<+],]++>>++<--[<++[+>]>+<<+++<]< +<[>>+[[>>+<<-]<<]>>>>[[<<+>.>-]>>]<.<<<+<<-]>>[<.>--]>.>>. + + From e605216b8c587d17de99e5c9c3acb5cc4dc9b4b7 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 17:26:02 +0000 Subject: [PATCH 6/8] fix(mir): preserve imported owned return provenance --- spec/allocation_strategy_spec.rb | 27 +++++++++ spec/doctor_spec.rb | 2 +- spec/mir_checker_spec.rb | 36 ++++++++++- src/annotator-helpers/function_analysis.rb | 3 +- src/annotator-helpers/function_signature.rb | 34 +++++++++++ src/ast/diagnostic_registry.rb | 7 +++ src/ast/std_lib.rb | 2 +- src/backends/compiler_frontend.rb | 19 +----- src/backends/importer.rb | 34 ++++------- src/mir/mir.rb | 7 +++ src/mir/mir_checker.rb | 60 +++++++++++++++---- src/mir/mir_emitter.rb | 2 +- src/mir/mir_lowering.rb | 35 +++++++---- src/mir/mir_pass.rb | 10 ++++ .../382_returned_list_cleanup_leak.cht | 15 +++++ .../382_returned_list_import_cleanup_leak.cht | 8 +++ transpile-tests/382_returned_list_lib.cht | 5 ++ 17 files changed, 239 insertions(+), 67 deletions(-) create mode 100644 transpile-tests/382_returned_list_cleanup_leak.cht create mode 100644 transpile-tests/382_returned_list_import_cleanup_leak.cht create mode 100644 transpile-tests/382_returned_list_lib.cht diff --git a/spec/allocation_strategy_spec.rb b/spec/allocation_strategy_spec.rb index 55cb58d29..0ef84f0f4 100644 --- a/spec/allocation_strategy_spec.rb +++ b/spec/allocation_strategy_spec.rb @@ -1,4 +1,5 @@ require "rspec" +require "tmpdir" require_relative "../src/backends/transpiler" # Allocation Strategy Verification — spec/allocation_strategy_spec.rb @@ -498,6 +499,32 @@ def cleanup_entry(fn, name) expect(cleanup_entry(caller_fn, "x")&.dig(:alloc)).to eq(:heap) end + it ":transitive_imported_callee — imported heap-returning call → cleanup :heap" do + Dir.mktmpdir("clear-import-owned-return") do |dir| + File.write(File.join(dir, "lib.cht"), <<~CLEAR) + FN build!() RETURNS !Float64[]@list -> + MUTABLE v: Float64[]@list = []; + v.append(1.0); + RETURN v; + END + CLEAR + + src = <<~CLEAR + REQUIRE "lib.cht"; + FN caller!() RETURNS !Void -> + x = build!(); + RETURN; + END + FN main() RETURNS Void -> RETURN; END + CLEAR + + importer = ModuleImporter.new(base_dir: dir, use_mir: true) + result = CompilerFrontend.compile(src, importer: importer, source_dir: dir) + caller_fn = result.ast.statements.find { |s| s.is_a?(AST::FunctionDef) && s.name == "caller!" } + expect(cleanup_entry(caller_fn, "x")&.dig(:alloc)).to eq(:heap) + end + end + end # =========================================================================== diff --git a/spec/doctor_spec.rb b/spec/doctor_spec.rb index 2716f2635..09e247fa1 100644 --- a/spec/doctor_spec.rb +++ b/spec/doctor_spec.rb @@ -73,7 +73,7 @@ def capture_stdout expect(sites.first).to include(allocs: 200, bytes: 4000, frees: 0, free_bytes: 0, live: 4000) expect(out).to include("Allocation Profile (12,345 allocs)") expect(out).to include("*** WARNING: 2 allocation samples dropped") - expect(out).to include("Top sites by bytes:") + expect(out).to match(/Top sites by (bytes|in-use bytes \(alloc - free\)):/) expect(out).to include("(heap rc) = @multiowned RC allocation tracked by rcCreate") end end diff --git a/spec/mir_checker_spec.rb b/spec/mir_checker_spec.rb index 4f0c341cc..b78591d30 100644 --- a/spec/mir_checker_spec.rb +++ b/spec/mir_checker_spec.rb @@ -34,12 +34,44 @@ def fn_def(name, body) it "passes for heap-returning call bound to Let" do call = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) body = [ + MIR::AllocMark.new("x", :heap), MIR::Let.new("x", call, false, nil, nil), + MIR::Cleanup.new("x", { kind: :heap_slice, alloc: :heap, has_moved_guard: false }), ] errors = checker.check_fn!(fn_def("hpt_ok", body)) expect(errors).to be_empty end + it "detects bound heap-returning call with no AllocMark" do + call = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) + body = [ + MIR::Let.new("x", call, false, nil, nil), + ] + errors = checker.check_fn!(fn_def("hpt_bound_no_alloc", body)) + expect(errors.any? { |e| e.include?("OWNED_RETURN_WITHOUT_ALLOC") && e.include?("x") }).to be true + end + + it "passes for bound heap-returning call transferred out of scope" do + call = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) + body = [ + MIR::AllocMark.new("x", :heap), + MIR::Let.new("x", call, false, nil, nil), + MIR::TransferMark.new("x", :moved), + ] + errors = checker.check_fn!(fn_def("hpt_bound_transfer", body)) + expect(errors).to be_empty + end + + it "detects bound heap-returning call marked allocated but neither cleaned nor transferred" do + call = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) + body = [ + MIR::AllocMark.new("x", :heap), + MIR::Let.new("x", call, false, nil, nil), + ] + errors = checker.check_fn!(fn_def("hpt_bound_alloc_only", body)) + expect(errors.any? { |e| e.include?("ALLOC_WITHOUT_CLEANUP") && e.include?("x") }).to be true + end + it "passes for ExprStmt with non-heap call" do call = MIR::Call.new("doWork", [MIR::Ident.new("rt")], false) body = [ @@ -60,7 +92,7 @@ def fn_def(name, body) end it "detects discarded InlineZig stdlib call with allocates:true" do - iz = MIR::InlineZig.new("CheatLib.clone({0})", "clone", nil, { allocates: true, return: :String }) + iz = MIR::InlineZig.new("CheatLib.clone({0})", "clone", nil, { allocates: true, return: :String, return_alloc: :heap }) body = [ MIR::ExprStmt.new(iz, false), ] @@ -720,7 +752,9 @@ def loop_restore_defer it "collects errors across multiple functions" do call1 = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) fn1 = fn_def("good", [ + MIR::AllocMark.new("x", :heap), MIR::Let.new("x", call1, false, nil, nil), + MIR::Cleanup.new("x", { kind: :heap_slice, alloc: :heap, has_moved_guard: false }), ]) call2 = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) diff --git a/src/annotator-helpers/function_analysis.rb b/src/annotator-helpers/function_analysis.rb index 14515b201..34dc5de96 100644 --- a/src/annotator-helpers/function_analysis.rb +++ b/src/annotator-helpers/function_analysis.rb @@ -279,7 +279,8 @@ def resolve_call(node, args) # frameAlloc internally — the caller shouldn't try to free those. if node.type_info.is_a?(Type) callee_node = @fn_nodes[func_name] - if callee_node&.return_provenance == :heap + sig_return_heap = func_type.is_a?(FunctionSignature) && func_type.return_provenance == :heap + if callee_node&.return_provenance == :heap || sig_return_heap node.type_info.provenance = :heap if node.type_info.is_a?(Type) elsif node.type_info&.needs_escape_promotion? && !node.type_info&.string? node.type_info.provenance = :heap if node.type_info.is_a?(Type) diff --git a/src/annotator-helpers/function_signature.rb b/src/annotator-helpers/function_signature.rb index b78e29284..adb03f65c 100644 --- a/src/annotator-helpers/function_signature.rb +++ b/src/annotator-helpers/function_signature.rb @@ -29,6 +29,39 @@ class FunctionSignature # checks survive cross-module flow. attr_accessor :requires + sig { params(fn: T.untyped).returns(FunctionSignature) } + def self.from_function_def(fn) + raw_sig = fn.full_type + raw_sig = raw_sig.raw if raw_sig.is_a?(Type) && raw_sig.raw.is_a?(FunctionSignature) + + sig = if raw_sig.is_a?(FunctionSignature) + raw_sig.dup + else + FunctionSignature.new( + params: fn.params || [], + return_type: fn.return_type || :Any, + return_lifetime: fn.return_lifetime, + visibility: fn.visibility, + type_params: fn.type_params, + reentrant: fn.reentrant == :reentrant + ) + end + + sync_from_function_def!(sig, fn) + end + + sig { params(sig: FunctionSignature, fn: T.untyped).returns(FunctionSignature) } + def self.sync_from_function_def!(sig, fn) + sig.needs_rt = fn.needs_rt if fn.respond_to?(:needs_rt) + sig.can_fail = fn.can_fail if fn.respond_to?(:can_fail) + sig.return_provenance = fn.return_provenance if fn.respond_to?(:return_provenance) + sig.effects = fn.effects if fn.respond_to?(:effects) + sig.requires = fn.requires if fn.respond_to?(:requires) + sig.return_strategy = fn.return_strategy if fn.respond_to?(:return_strategy) + sig.stack_tier = fn.stack_tier if fn.respond_to?(:stack_tier) + sig + end + sig { params(params: T::Array[Hash], return_type: T.untyped, return_lifetime: T.untyped, visibility: T.nilable(Symbol), type_params: T.nilable(Array), reentrant: T::Boolean, extern: T::Boolean, module_alias: T.nilable(String), extern_effects: T.nilable(T::Hash[Symbol, T.untyped]), fn_type_params: T.nilable(T::Array[Symbol]), owner_type: T.nilable(String), owner_type_params: T.nilable(Array), intrinsic: T::Boolean, zig_pattern: T.nilable(String)).void } def initialize(params:, return_type:, return_lifetime: nil, visibility: nil, type_params: nil, reentrant: false, extern: false, @@ -67,6 +100,7 @@ def dup s.effects = @effects s.return_strategy = @return_strategy s.stack_tier = @stack_tier + s.requires = @requires end end end diff --git a/src/ast/diagnostic_registry.rb b/src/ast/diagnostic_registry.rb index fd1029773..7035b8f82 100644 --- a/src/ast/diagnostic_registry.rb +++ b/src/ast/diagnostic_registry.rb @@ -1857,6 +1857,13 @@ module DiagnosticRegistry cause: "Every MIR node that allocates memory (DupeSlice, HeapCreate, ConcatStr, AllocSlice, MakeList, CapWrap, SharePromote, deep DeepCopy, ContainerInit) must appear as the direct init of a `MIR::Let` so it has an AllocMark. Found one in argument / return / field-value position instead.", fix_hint: "Lowering bug — HPT hoisting (hoist_alloc) should have lifted the call into a Let. Check the producer pass that emitted the allocating node; it should bind the result to a fresh local.", }, + OWNED_RETURN_WITHOUT_ALLOC: { + severity: :error, category: :mir, + template: "%{message}", + summary: "Owned-return call is bound without a checker-visible AllocMark.", + cause: "A call whose return value owns heap data was lowered directly into a binding, but the binding has no MIR::AllocMark. Without the marker, MIRChecker cannot verify that cleanup exists on every path.", + fix_hint: "Lowering bug — preserve return provenance through FunctionSignature import/reconstruction and make the cleanup classifier emit AllocMark + Cleanup for the binding.", + }, COPY_CLEANUP: { severity: :error, category: :mir, template: "%{message}", diff --git a/src/ast/std_lib.rb b/src/ast/std_lib.rb index 4f52aab25..858aa80ab 100644 --- a/src/ast/std_lib.rb +++ b/src/ast/std_lib.rb @@ -221,7 +221,7 @@ else recv_t.element_type end - Type.new(:"#{elem_t.resolved}[]", collection: :list) + Type.new(:"#{elem_t.resolved}[]", collection: :list, location: :heap) }, zig: "try ({0}).toList({rt}.heapAlloc())", bc: true, bc_op: :to_list, diff --git a/src/backends/compiler_frontend.rb b/src/backends/compiler_frontend.rb index d875ddadb..ac705566a 100644 --- a/src/backends/compiler_frontend.rb +++ b/src/backends/compiler_frontend.rb @@ -79,24 +79,7 @@ def self.compile(cheat_code, importer:, source_dir:, strict_test: false) fn_sigs = {} T.must(ast).statements.each do |stmt| next unless stmt.is_a?(AST::FunctionDef) - sig = stmt.full_type - if sig.is_a?(FunctionSignature) - fn_sigs[stmt.name] = sig - else - fs = FunctionSignature.new( - params: stmt.params, - return_type: stmt.return_type || :Any, - return_lifetime: stmt.return_lifetime, - visibility: stmt.visibility, - type_params: stmt.type_params, - reentrant: stmt.reentrant == :reentrant - ) - fs.needs_rt = stmt.needs_rt - fs.can_fail = stmt.can_fail - fs.effects = stmt.effects - fs.requires = stmt.requires - fn_sigs[stmt.name] = fs - end + fn_sigs[stmt.name] = FunctionSignature.from_function_def(stmt) end # Include module-imported function signatures so MIRLowering can diff --git a/src/backends/importer.rb b/src/backends/importer.rb index b7d2b4a0d..6cba19236 100644 --- a/src/backends/importer.rb +++ b/src/backends/importer.rb @@ -175,6 +175,7 @@ def compile_module_mir(ast, annotator, source_dir) ast.statements.each { |s| fn_nodes[s.name] = s if s.is_a?(AST::FunctionDef) } mir_pass = MIRPass.new(fn_nodes: fn_nodes, schema_lookup: schema_lookup) mir_pass.transform!(ast) + sync_global_scope_function_signatures!(ast, annotator) # Collect schemas struct_schemas = {} @@ -191,27 +192,7 @@ def compile_module_mir(ast, annotator, source_dir) fn_sigs = {} ast.statements.each do |stmt| next unless stmt.is_a?(AST::FunctionDef) - sig = stmt.full_type - if sig.is_a?(FunctionSignature) - fn_sigs[stmt.name] = sig - else - # The annotator stamps `full_type` as a `Type` for some imported - # defs (cross-file REQUIREd helpers among them). Reconstruct - # from the FunctionDef's own param list so call-site routing - # decisions that consult `callee_sig.params[idx]` (MUTABLE @list - # detection, takes/borrow/etc.) work for cross-file callees too. - # Without this, the lowering silently sees an empty param list - # and emits the wrong arg shape (e.g. `.items` instead of `&xs` - # for a MUTABLE @list parameter). - fs = FunctionSignature.new( - params: stmt.params || [], - return_type: stmt.return_type || :Any - ) - fs.needs_rt = stmt.needs_rt - fs.can_fail = stmt.can_fail - fs.effects = stmt.effects - fn_sigs[stmt.name] = fs - end + fn_sigs[stmt.name] = FunctionSignature.from_function_def(stmt) end moved_guard_info = {} @@ -243,4 +224,15 @@ def compile_module_mir(ast, annotator, source_dir) type_defs ) end + + sig { params(ast: AST::Program, annotator: SemanticAnnotator).returns(T.nilable(Hash)) } + def sync_global_scope_function_signatures!(ast, annotator) + ast.statements.each do |stmt| + next unless stmt.is_a?(AST::FunctionDef) + entry = annotator.scope_stack.first.locals[stmt.name] + next unless entry&.type.is_a?(FunctionSignature) + FunctionSignature.sync_from_function_def!(entry.type, stmt) + end + nil + end end diff --git a/src/mir/mir.rb b/src/mir/mir.rb index 13a47daa8..eecfb1914 100644 --- a/src/mir/mir.rb +++ b/src/mir/mir.rb @@ -1267,6 +1267,13 @@ def stmt?; true; end def stmt?; true; end end + # Marks an owned binding whose local cleanup is intentionally absent because + # ownership transfers out of the current scope (TAKES arg, return, container). + TransferMark = Struct.new(:name, :target) do + include Stmt + def stmt?; true; end + end + # Marks reassignment needing pre-cleanup. Subsumes old MIR::ReassignCleanup. ReassignMark = Struct.new(:name, :alloc) do include Stmt diff --git a/src/mir/mir_checker.rb b/src/mir/mir_checker.rb index 1cf7212f9..e980719f0 100644 --- a/src/mir/mir_checker.rb +++ b/src/mir/mir_checker.rb @@ -5,7 +5,9 @@ # # INV-ALLOC-CLEANUP: Every MIR::AllocMark has at least one matching # MIR::Cleanup or MIR::ErrCleanup for the same binding name, and -# the allocators match. (HPT_LEAK is the leak-without-alloc case.) +# the allocators match, unless a MIR::TransferMark records that +# ownership left the current scope. (HPT_LEAK is the leak-without-alloc +# case.) # # INV-CLEANUP-ALLOC: Every MIR::Cleanup or MIR::ErrCleanup has a # matching MIR::AllocMark. A cleanup with no alloc is a compiler bug. @@ -52,6 +54,8 @@ # MIR::Cleanup -> always-defer cleanup (freed on both success and error) # MIR::ErrCleanup -> errdefer-only cleanup (freed only on error; success # path transfers ownership to caller/container/callee) +# MIR::TransferMark -> no local cleanup because ownership was moved out of +# this scope on every successful path # # Which type is emitted is determined by the lowering pass, not the checker. # The checker does NOT inspect flags or tags -- it reads the node type. @@ -89,8 +93,10 @@ def check_fn!(fn_def, strict: false) allocs = {} cleanups = {} + transfers = Set.new errdefer_destroy_names = Set.new hpt_leaks = [] + owned_return_lets = [] inline_alloc_nodes = [] all_zig_nodes = [] # InlineZig + RawZig -- both scanned for CheatLib contracts @@ -100,6 +106,8 @@ def check_fn!(fn_def, strict: false) (allocs[node.name] ||= []) << node when MIR::Cleanup, MIR::ErrCleanup (cleanups[node.name] ||= []) << node + when MIR::TransferMark + transfers << node.name when MIR::ErrDeferStmt # @indirect field temps use ErrDeferStmt(DestroyPtr) instead of ErrCleanup. # Track their names so ALLOC_WITHOUT_CLEANUP does not false-positive on them. @@ -107,6 +115,7 @@ def check_fn!(fn_def, strict: false) errdefer_destroy_names << node.body.ptr.name end when MIR::Let + owned_return_lets << node if owned_return_init?(node.init) all_zig_nodes << node.init if node.init.is_a?(MIR::InlineZig) when MIR::ExprStmt scan_expr_for_hpt_leak!(node.expr, hpt_leaks) @@ -130,9 +139,10 @@ def check_fn!(fn_def, strict: false) end hpt_leaks.each { |e| @errors << e } + verify_owned_return_alloc_marks!(owned_return_lets, allocs) verify_inline_alloc_contracts!(inline_alloc_nodes, allocs) verify_cross_frame_param_alloc!(inline_alloc_nodes, fn_def) - verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names) + verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names, transfers) verify_zig_contracts!(all_zig_nodes) verify_raw_justified!(all_zig_nodes) verify_frame_rewind!(fn_def.body) @@ -151,6 +161,34 @@ def check_program!(program, strict: false) all_errors end + sig { params(init: T.untyped).returns(T::Boolean) } + def owned_return_init?(init) + return true if init.is_a?(MIR::Call) && init.heap_provenance + if init.is_a?(MIR::InlineZig) || init.is_a?(MIR::RawZig) + return false unless stdlib_owned_return?(init) + ret = init.stdlib_def[:return] + return !(ret == :Void || ret.nil?) + end + false + end + + sig { params(node: T.untyped).returns(T::Boolean) } + def stdlib_owned_return?(node) + return false unless node.stdlib_def&.dig(:allocates) + return true if node.stdlib_def[:return_alloc] == :heap + allocs = node.respond_to?(:allocs) ? node.allocs : nil + allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap } + end + + sig { params(lets: T::Array[MIR::Let], allocs: T::Hash[String, Array]).returns(T.nilable(Array)) } + def verify_owned_return_alloc_marks!(lets, allocs) + lets.each do |let| + next if allocs.key?(let.name) + @errors << error(:OWNED_RETURN_WITHOUT_ALLOC, let.name, + "owned-return initializer is bound without MIR::AllocMark; cleanup cannot be verified") + end + end + # =================================================================== # FSM structural validation # =================================================================== @@ -331,7 +369,7 @@ def scan_expr_for_hpt_leak!(node, leaks) leaks << error(:HPT_LEAK, node.callee, "heap-returning call result not bound to variable (leak)") end - if (node.is_a?(MIR::InlineZig) || node.is_a?(MIR::RawZig)) && node.stdlib_def&.dig(:allocates) + if (node.is_a?(MIR::InlineZig) || node.is_a?(MIR::RawZig)) && stdlib_owned_return?(node) ret = node.stdlib_def[:return] unless ret == :Void || ret.nil? label = node.is_a?(MIR::RawZig) ? "RawZig block" : "stdlib call" @@ -427,14 +465,13 @@ def verify_cross_frame_param_alloc!(inline_nodes, fn_def) # call heapAlloc().free() on frame memory or vice versa -> runtime crash. # # Only checks bindings that have BOTH an AllocMark and a Cleanup. Bindings with - # only an AllocMark were moved/escaped (no local free needed). Bindings with only - # a Cleanup indicate a missing AllocMark -- every locally-allocated binding + # only a Cleanup indicate a missing AllocMark -- every locally-allocated binding # (including TAKES params via insert_takes_drops! and heap carry vars via # insert_drop!) must have a corresponding AllocMark. A Cleanup with no AllocMark # is a compiler bug: the allocation event is invisible to the checker, so # ALLOC_CLEANUP_MISMATCH cannot fire even if the allocators diverge. - sig { params(allocs: T::Hash[String, Array], cleanups: T::Hash[String, Array], errdefer_destroy_names: Set).returns(T::Hash[String, Array]) } - def verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names = Set.new) + sig { params(allocs: T::Hash[String, Array], cleanups: T::Hash[String, Array], errdefer_destroy_names: Set, transfers: Set).returns(T::Hash[String, Array]) } + def verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names = Set.new, transfers = Set.new) allocs.each do |name, alloc_marks| next unless cleanups.key?(name) @@ -469,16 +506,17 @@ def verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names = Set.n "MIR::Cleanup present but no MIR::AllocMark (allocation event missing from MIR)") end - # ALLOC_WITHOUT_CLEANUP: every HEAP AllocMark must have a Cleanup, ErrCleanup, or - # ErrDeferStmt(DestroyPtr). Frame allocations are freed by the arena rewind and - # do not require an explicit cleanup node. + # ALLOC_WITHOUT_CLEANUP: every HEAP AllocMark must have a Cleanup, ErrCleanup, + # ErrDeferStmt(DestroyPtr), or explicit TransferMark. Frame allocations are + # freed by the arena rewind and do not require an explicit cleanup node. # Exception: @indirect field temps use ErrDeferStmt(DestroyPtr) (errdefer_destroy_names). allocs.each do |name, alloc_marks| next if cleanups.key?(name) next if errdefer_destroy_names.include?(name) + next if transfers.include?(name) next if alloc_marks.all? { |m| m.alloc == :frame } @errors << error(:ALLOC_WITHOUT_CLEANUP, name, - "AllocMark with no Cleanup, ErrCleanup, or ErrDeferStmt(DestroyPtr) -- leaked allocation") + "AllocMark with no Cleanup, ErrCleanup, ErrDeferStmt(DestroyPtr), or TransferMark -- leaked allocation") end end diff --git a/src/mir/mir_emitter.rb b/src/mir/mir_emitter.rb index 485a14663..3ab54faed 100644 --- a/src/mir/mir_emitter.rb +++ b/src/mir/mir_emitter.rb @@ -114,7 +114,7 @@ def emit(node) when MIR::PolymorphicMutateFlow then emit_polymorphic_mutate_flow(node) when MIR::WithMatchDispatch then emit_with_match_dispatch(node) # --- Verification-only (no codegen) --- - when MIR::AllocMark, MIR::ReturnMark, MIR::ReassignMark, MIR::FieldCleanupMark + when MIR::AllocMark, MIR::ReturnMark, MIR::TransferMark, MIR::ReassignMark, MIR::FieldCleanupMark nil # --- Expressions --- diff --git a/src/mir/mir_lowering.rb b/src/mir/mir_lowering.rb index 0b4688f9d..419e0ff24 100644 --- a/src/mir/mir_lowering.rb +++ b/src/mir/mir_lowering.rb @@ -4330,18 +4330,7 @@ def lower_require(node) if T.must(mod).ast T.must(mod).ast.statements.each do |stmt| next unless stmt.is_a?(AST::FunctionDef) - sig = stmt.full_type - if sig.is_a?(FunctionSignature) - @fn_sigs[stmt.name] = sig - else - fs = FunctionSignature.new( - params: stmt.params || [], - return_type: stmt.return_type || :Any - ) - fs.needs_rt = stmt.needs_rt - fs.can_fail = stmt.can_fail - @fn_sigs[stmt.name] = fs - end + @fn_sigs[stmt.name] = FunctionSignature.from_function_def(stmt) end end @@ -6131,11 +6120,33 @@ def lower_var_decl(node) drop_entry[:alloc] = node_alloc mir_alloc = resolve_decl_stdlib_alloc(node) || node_alloc [MIR::AllocMark.new(safe_name, mir_alloc, node.type_info), let_node, MIR::Cleanup.new(safe_name, drop_entry)] + elsif owned_return_transfer_binding?(binding_entry, init) + mir_alloc = resolve_decl_stdlib_alloc(node) || binding_entry[:alloc] || :heap + [MIR::AllocMark.new(safe_name, mir_alloc, node.type_info), let_node, MIR::TransferMark.new(safe_name, :moved)] else let_node end end + sig { params(binding_entry: T.untyped, init: T.untyped).returns(T::Boolean) } + def owned_return_transfer_binding?(binding_entry, init) + return false unless binding_entry && binding_entry[:needs_cleanup] == false + return false unless binding_entry[:alloc] == :heap || binding_entry[:alloc] == :cleanup + + if init.is_a?(MIR::Call) + return !!init.heap_provenance + end + + if init.is_a?(MIR::InlineZig) || init.is_a?(MIR::RawZig) + return false unless init.stdlib_def&.dig(:allocates) + return true if init.stdlib_def[:return_alloc] == :heap + allocs = init.respond_to?(:allocs) ? init.allocs : nil + return allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap } + end + + false + end + sig { params(node: AST::BindExpr).returns(T.untyped) } def lower_bind_expr(node) if node.mode == :decl diff --git a/src/mir/mir_pass.rb b/src/mir/mir_pass.rb index be787900a..e24c02c99 100644 --- a/src/mir/mir_pass.rb +++ b/src/mir/mir_pass.rb @@ -94,6 +94,16 @@ def transform!(ast) next unless fn&.body transform_function!(fn, promotion_plans[name]) end + + # MIR escape analysis can discover heap-return provenance after the + # annotator created each FunctionSignature. Resync the signature objects + # so cross-module imports and later lowering see the same ownership facts + # as the FunctionDef. + @fn_nodes.each_value do |fn| + sig = fn.full_type + sig = sig.raw if sig.is_a?(Type) && sig.raw.is_a?(FunctionSignature) + FunctionSignature.sync_from_function_def!(sig, fn) if sig.is_a?(FunctionSignature) + end end private diff --git a/transpile-tests/382_returned_list_cleanup_leak.cht b/transpile-tests/382_returned_list_cleanup_leak.cht new file mode 100644 index 000000000..f64672e89 --- /dev/null +++ b/transpile-tests/382_returned_list_cleanup_leak.cht @@ -0,0 +1,15 @@ +# Repro: returning a heap-backed list from a helper and keeping it in the +# caller should not leak under the debug allocator. + +FN makeNumbers() RETURNS !Int64[]@list -> + MUTABLE xs: Int64[]@list = []; + xs.append(2); + RETURN xs; +END + +FN main() RETURNS Void -> + nums = makeNumbers(); + ASSERT nums.length() == 1, "returned list length"; + ASSERT nums[0] == 2, "returned list contents"; + RETURN; +END diff --git a/transpile-tests/382_returned_list_import_cleanup_leak.cht b/transpile-tests/382_returned_list_import_cleanup_leak.cht new file mode 100644 index 000000000..d379ed4e9 --- /dev/null +++ b/transpile-tests/382_returned_list_import_cleanup_leak.cht @@ -0,0 +1,8 @@ +REQUIRE "382_returned_list_lib.cht"; + +FN main() RETURNS Void -> + nums = makeNumbersFromLib(); + ASSERT nums.length() == 1, "imported returned list length"; + ASSERT nums[0] == 2, "imported returned list contents"; + RETURN; +END diff --git a/transpile-tests/382_returned_list_lib.cht b/transpile-tests/382_returned_list_lib.cht new file mode 100644 index 000000000..4814e8fd5 --- /dev/null +++ b/transpile-tests/382_returned_list_lib.cht @@ -0,0 +1,5 @@ +FN makeNumbersFromLib() RETURNS !Int64[]@list -> + MUTABLE xs: Int64[]@list = []; + xs.append(2); + RETURN xs; +END From 5fe99298657d5d854d2db33ebdb96db4aaafd713 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 17:30:15 +0000 Subject: [PATCH 7/8] fix(mir): validate owned transfer markers --- spec/mir_checker_spec.rb | 19 +++++++++++++++++++ src/ast/diagnostic_registry.rb | 14 ++++++++++++++ src/mir/mir_checker.rb | 26 ++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/spec/mir_checker_spec.rb b/spec/mir_checker_spec.rb index b78591d30..81ac69445 100644 --- a/spec/mir_checker_spec.rb +++ b/spec/mir_checker_spec.rb @@ -72,6 +72,25 @@ def fn_def(name, body) expect(errors.any? { |e| e.include?("ALLOC_WITHOUT_CLEANUP") && e.include?("x") }).to be true end + it "detects heap-returning binding marked as frame-allocated" do + call = MIR::Call.new("makeList", [MIR::Ident.new("rt")], false, true) + body = [ + MIR::AllocMark.new("x", :frame), + MIR::Let.new("x", call, false, nil, nil), + MIR::Cleanup.new("x", { kind: :heap_slice, alloc: :frame, has_moved_guard: false }), + ] + errors = checker.check_fn!(fn_def("hpt_bound_frame_alloc", body)) + expect(errors.any? { |e| e.include?("OWNED_RETURN_ALLOC_NOT_HEAP") && e.include?("x") }).to be true + end + + it "detects transfer marker with no allocation source" do + body = [ + MIR::TransferMark.new("x", :moved), + ] + errors = checker.check_fn!(fn_def("transfer_without_alloc", body)) + expect(errors.any? { |e| e.include?("TRANSFER_WITHOUT_ALLOC") && e.include?("x") }).to be true + end + it "passes for ExprStmt with non-heap call" do call = MIR::Call.new("doWork", [MIR::Ident.new("rt")], false) body = [ diff --git a/src/ast/diagnostic_registry.rb b/src/ast/diagnostic_registry.rb index 7035b8f82..4928b8d51 100644 --- a/src/ast/diagnostic_registry.rb +++ b/src/ast/diagnostic_registry.rb @@ -1864,6 +1864,20 @@ module DiagnosticRegistry cause: "A call whose return value owns heap data was lowered directly into a binding, but the binding has no MIR::AllocMark. Without the marker, MIRChecker cannot verify that cleanup exists on every path.", fix_hint: "Lowering bug — preserve return provenance through FunctionSignature import/reconstruction and make the cleanup classifier emit AllocMark + Cleanup for the binding.", }, + OWNED_RETURN_ALLOC_NOT_HEAP: { + severity: :error, category: :mir, + template: "%{message}", + summary: "Owned-return call has a non-heap AllocMark.", + cause: "A call whose return value owns heap data was paired with an AllocMark that says the value is frame-allocated. That lets AllocMark and Cleanup agree with each other while still freeing heap-owned data through the wrong allocator.", + fix_hint: "Lowering bug — the AllocMark for a heap-provenance return must use :heap or :cleanup, never :frame.", + }, + TRANSFER_WITHOUT_ALLOC: { + severity: :error, category: :mir, + template: "%{message}", + summary: "TransferMark exists without a matching AllocMark.", + cause: "MIR::TransferMark suppresses local cleanup because ownership left the current scope. Without a matching AllocMark, there is no checker-visible allocation event to prove what was transferred.", + fix_hint: "Lowering bug — emit TransferMark only alongside the AllocMark for the owned binding being moved.", + }, COPY_CLEANUP: { severity: :error, category: :mir, template: "%{message}", diff --git a/src/mir/mir_checker.rb b/src/mir/mir_checker.rb index e980719f0..ac7e813d3 100644 --- a/src/mir/mir_checker.rb +++ b/src/mir/mir_checker.rb @@ -55,7 +55,8 @@ # MIR::ErrCleanup -> errdefer-only cleanup (freed only on error; success # path transfers ownership to caller/container/callee) # MIR::TransferMark -> no local cleanup because ownership was moved out of -# this scope on every successful path +# this scope on every successful path. Must pair with +# a matching AllocMark. # # Which type is emitted is determined by the lowering pass, not the checker. # The checker does NOT inspect flags or tags -- it reads the node type. @@ -183,9 +184,17 @@ def stdlib_owned_return?(node) sig { params(lets: T::Array[MIR::Let], allocs: T::Hash[String, Array]).returns(T.nilable(Array)) } def verify_owned_return_alloc_marks!(lets, allocs) lets.each do |let| - next if allocs.key?(let.name) - @errors << error(:OWNED_RETURN_WITHOUT_ALLOC, let.name, - "owned-return initializer is bound without MIR::AllocMark; cleanup cannot be verified") + marks = allocs[let.name] + unless marks + @errors << error(:OWNED_RETURN_WITHOUT_ALLOC, let.name, + "owned-return initializer is bound without MIR::AllocMark; cleanup cannot be verified") + next + end + + if marks.any? { |m| m.alloc == :frame } + @errors << error(:OWNED_RETURN_ALLOC_NOT_HEAP, let.name, + "owned-return initializer is heap-provenance but MIR::AllocMark uses :frame") + end end end @@ -506,6 +515,15 @@ def verify_alloc_cleanup_match!(allocs, cleanups, errdefer_destroy_names = Set.n "MIR::Cleanup present but no MIR::AllocMark (allocation event missing from MIR)") end + # TRANSFER_WITHOUT_ALLOC: a transfer marker is only meaningful if the value + # it transfers had an allocation event. Otherwise TransferMark can mask an + # untracked ownership path. + transfers.each do |name| + next if allocs.key?(name) + @errors << error(:TRANSFER_WITHOUT_ALLOC, name, + "MIR::TransferMark present but no MIR::AllocMark (transfer event missing allocation source)") + end + # ALLOC_WITHOUT_CLEANUP: every HEAP AllocMark must have a Cleanup, ErrCleanup, # ErrDeferStmt(DestroyPtr), or explicit TransferMark. Frame allocations are # freed by the arena rewind and do not require an explicit cleanup node. From 96ca8a27f8666fdadfc41d0ce789b286db22c398 Mon Sep 17 00:00:00 2001 From: Brian Yahn Date: Sat, 9 May 2026 17:35:47 +0000 Subject: [PATCH 8/8] fix(mir): satisfy sorbet for owned return checks --- src/mir/mir_checker.rb | 6 ++++-- src/mir/mir_lowering.rb | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/mir/mir_checker.rb b/src/mir/mir_checker.rb index ac7e813d3..aea53b0f9 100644 --- a/src/mir/mir_checker.rb +++ b/src/mir/mir_checker.rb @@ -177,8 +177,10 @@ def owned_return_init?(init) def stdlib_owned_return?(node) return false unless node.stdlib_def&.dig(:allocates) return true if node.stdlib_def[:return_alloc] == :heap - allocs = node.respond_to?(:allocs) ? node.allocs : nil - allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap } + return false unless node.is_a?(MIR::InlineZig) + + allocs = node.allocs + !!(allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap }) end sig { params(lets: T::Array[MIR::Let], allocs: T::Hash[String, Array]).returns(T.nilable(Array)) } diff --git a/src/mir/mir_lowering.rb b/src/mir/mir_lowering.rb index 419e0ff24..11434e163 100644 --- a/src/mir/mir_lowering.rb +++ b/src/mir/mir_lowering.rb @@ -6140,8 +6140,10 @@ def owned_return_transfer_binding?(binding_entry, init) if init.is_a?(MIR::InlineZig) || init.is_a?(MIR::RawZig) return false unless init.stdlib_def&.dig(:allocates) return true if init.stdlib_def[:return_alloc] == :heap - allocs = init.respond_to?(:allocs) ? init.allocs : nil - return allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap } + return false unless init.is_a?(MIR::InlineZig) + + allocs = init.allocs + return !!(allocs.is_a?(Hash) && allocs.values.any? { |v| v == :heap }) end false