Skip to content

Commit 40627f2

Browse files
Host-side autofixer + VM closures + direct-call benchmarks
Three more architectural moves: Track 1 — Direct-call benchmark variant: examples/benchmarks.omc gains a second loop that calls each benchmark function directly (bench_int_add(N)) instead of through call(fn, args). Comparison reveals exactly where the Rust VM advantage lives. Headline number: recursive fib(22) drops from 2.3 ms/op (tree-walk) to 0.95 ms/op (VM direct call) — a 2.42x speedup where Op::Call dispatch dominates. Reflective dispatch via call(fn, args) is ~identical between tree-walk and VM because it always routes through invoke_user_function (tree-walk). The benchmark suite now produces actionable signal for future VM work. Track 4 — Host-side autofixer (OMC_HEAL=1): The H.1-H.5 healing logic was previously stuck inside OMC demo files. Now it's a toolchain feature: OMC_HEAL=1 walks the AST after parsing, applies four classes of rewrites, prints diagnostics to stderr, and runs the healed AST. Classes: Harmonic: numeric literal close to Fibonacci (|Δ| ≤ 3) → rewrite to nearest attractor Typo: call to unknown name, Levenshtein within 2 → resolve to best match (user fns preferred over builtins as tiebreaker — fbi → fib, not fbi → pi) /0: literal divide-by-zero → Call("safe_divide", [x, 0]) Arity: user-fn call with wrong arity → pad with 0 literals (too few) → truncate (too many) Only fires on user fns whose declared arity we know. ~250 lines added to interpreter.rs: heal_ast, heal_stmt, heal_expr, plus module-level helpers (edit_distance, closest_name, is_on_fibonacci_attractor, HEAL_BUILTIN_NAMES). OMC_HEAL_QUIET=1 suppresses the diagnostic preamble. Demo: examples/heal_pass_demo.omc — 8 diagnostics, all four classes exercised, healed program executes cleanly. Track 2 — Closures on the Rust VM (MVP): Lambdas previously errored under OMC_VM=1. Now they compile: New Op::Lambda(name) opcode. Compile-time: Expression::Lambda registers body as CompiledFunction in module.functions AND stashes the AST body in a new module.lambda_asts field. Runtime: pushes Value::Function with name + captured = current scope (Rc). Sibling lambdas share captured Rc. main.rs registers every module.lambda_asts entry into the interpreter's function table before vm.run_module(). Closure invocation routes through call_first_class_function → invoke_user_function (tree-walk for body); registration makes the AST body discoverable. Body execution still tree-walks — fast bytecode-VM body execution is future work. But the CREATE step is bytecode- native, and OMC_VM=1 works end-to-end on programs that use lambdas now. Verified: examples/test_runner.omc (uses inline lambdas for arr_filter) runs cleanly under OMC_VM=1 — 5/6 tests pass. Bank-account pattern: identical output on both paths. Architectural side-effects: Module gains lambda_asts: Vec<(String, Vec<String>, Vec<Statement>)>. Default empty so existing callers don't break. Compiler gains pending_lambda_asts; nested compilers drain into their parent. Interpreter gains pub register_lambda(name, params, body). Op::Lambda(String) has a disassembly form. Regression: V.9b ✓✓✓ unchanged. H.5: 6/6 converge. test_runner: 5/6 pass on BOTH tree-walk and OMC_VM=1. safe_keyword_host, module_demo, mutable_closure_test, benchmarks all good. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f9e1a7a commit 40627f2

11 files changed

Lines changed: 708 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,82 @@ All notable changes to OMNIcode will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added (Host-side autofixer + VM closures + direct-call benchmarks, 2026-05-14)
8+
9+
🎯 **The healer becomes a toolchain feature; lambdas work on the Rust VM; direct-call benchmark variant reveals the VM's 2.4× speedup on recursion.**
10+
11+
#### Track 1 — Direct-call benchmark variant
12+
13+
Added a second benchmark loop to `examples/benchmarks.omc` that calls each function directly (`bench_int_add(N)`) instead of through `call(fn, args)`. The two loops together reveal exactly where the Rust VM advantage lives.
14+
15+
**Result on a modern laptop:**
16+
17+
| Operation | Tree-walk | VM reflective | VM direct | Speedup |
18+
|---|---|---|---|---|
19+
| `int_add` (sum 0..N) | 425 ns/op | 420 ns/op | 375 ns/op | 1.13× |
20+
| `int_mul` | 505 | 485 | 430 | 1.17× |
21+
| `is_fibonacci` | 360 | 340 | 280 | 1.29× |
22+
| `recursive fib(22)` | 2.3 ms | 2.3 ms | **0.95 ms** | **2.42×** |
23+
24+
The big finding: reflective dispatch (`call(fn, args)`) routes through tree-walk regardless of `OMC_VM`. **Direct calls hit the bytecode VM hot path** — and `recursive fib(22)` shows a 2.4× speedup, where the Op::Call cycle dominates. The benchmark suite now produces actionable signal for future VM work.
25+
26+
#### Track 4 — Host-side autofixer (`OMC_HEAL=1`)
27+
28+
The H.1–H.5 self-healing demos lived inside OMC programs — you'd run `self_healing_h5.omc` and it healed a hardcoded broken-source string. Useful as a research demonstration, but you couldn't apply it to your own code.
29+
30+
This commit lifts the healing pass into the **host toolchain**. `OMC_HEAL=1` walks the AST after parsing, applies four classes of rewrites, prints diagnostics to stderr, then executes the healed AST.
31+
32+
```
33+
$ OMC_HEAL=1 ./target/release/omnimcode-standalone examples/heal_pass_demo.omc
34+
--- OMC_HEAL: 8 diagnostic(s) ---
35+
harmonic: 145 not Fibonacci → 144 (|Δ|=1)
36+
divide-by-zero: rewriting to safe_divide(...)
37+
call: 'fbi' unknown → 'fib'
38+
harmonic: 7 not Fibonacci → 8 (|Δ|=1)
39+
arity: fib() called with 0 args, padded with 1 zeros to match arity 1
40+
harmonic: 10 not Fibonacci → 8 (|Δ|=2)
41+
harmonic: 20 not Fibonacci → 21 (|Δ|=1)
42+
arity: fib() called with 3 args, truncated 2 excess to match arity 1
43+
--- end OMC_HEAL ---
44+
100 # 100/0 → safe_divide(100, 0) = 100
45+
21 # fbi(7) → fib(8) = 21
46+
0 # fib() → fib(0) = 0
47+
21 # fib(10,20,30) → fib(8) = 21 (extras truncated, harmonic-healed first)
48+
```
49+
50+
The classes implemented:
51+
- **Harmonic** (literal close to Fibonacci): rewrite to nearest attractor when `|Δ| ≤ 3`.
52+
- **Identifier typo at call site**: Levenshtein within distance 2; tiebreaker prefers user-defined functions over builtins. This catches `fbi → fib` (not `fbi → pi`, which is also distance 2 but is a builtin).
53+
- **Literal divide-by-zero**: `x / 0``Call("safe_divide", [x, 0])`.
54+
- **Arity auto-pad / truncate (H.6)**: user-fn call with too few args → pad with `0` literals; too many → truncate. Only fires on user functions (we know their declared arity).
55+
56+
The implementation is ~250 lines in `interpreter.rs``heal_ast`, `heal_stmt`, `heal_expr`, plus module-level helpers `edit_distance`, `closest_name`, `is_on_fibonacci_attractor`, and the `HEAL_BUILTIN_NAMES` static slice that keeps the typo-checker from flagging real builtins.
57+
58+
`OMC_HEAL_QUIET=1` suppresses the diagnostic preamble — heal still happens silently.
59+
60+
#### Track 2 — Closures on the Rust VM (MVP)
61+
62+
Lambdas previously errored under `OMC_VM=1` with "Lambda expressions require tree-walk." Now they compile:
63+
64+
- New `Op::Lambda(name)` opcode. Compile-time: `Expression::Lambda { params, body }` registers the body as a `CompiledFunction` in `module.functions` under a fresh `__lambda_N` name AND stashes the AST body in a new `module.lambda_asts` field. Runtime: pushes a `Value::Function` with `name` and `captured = Some(self.locals.last().cloned())` — sibling lambdas share the captured Rc.
65+
- `main.rs` registers every entry in `module.lambda_asts` into the interpreter's function table before `vm.run_module(...)`. Closure invocation routes through `call_first_class_function → invoke_user_function` (tree-walk semantics for the body), so this registration makes the body discoverable.
66+
- Body execution still routes through tree-walk — fast bytecode-VM body execution is future work. But the COMPILE and CREATE steps are now bytecode-native, and `OMC_VM=1` works end-to-end on programs that use lambdas.
67+
68+
Verified: `examples/test_runner.omc` (which uses inline lambdas for `arr_filter`) runs cleanly under `OMC_VM=1` — 5/6 tests pass (the intentional failure still fires).
69+
70+
Bank-account pattern produces identical output on both interpretation paths (100, 150, 120, 120).
71+
72+
#### Architectural side-effects
73+
74+
- `Module` gained a `lambda_asts: Vec<(String, Vec<String>, Vec<Statement>)>` field. Doesn't break existing callers because `Module::default()` returns empty.
75+
- `Compiler` gained a `pending_lambda_asts` field that nested compilers drain into their parent.
76+
- `Interpreter` gained a public `register_lambda(name, params, body)` method, used by `main.rs` when running in VM mode.
77+
- New `Op::Lambda(String)` disassembly form.
78+
79+
#### Regression
80+
81+
V.9b ✓✓✓ unchanged. H.5: 6/6 demos converge. Test runner: 5/6 (1 intentional failure) on BOTH tree-walk and `OMC_VM=1`. `safe_keyword_host`, `module_demo`, `mutable_closure_test`, `benchmarks` all produce expected output. `heal_pass_demo` heals 8 issues and runs to completion.
82+
783
### Added (Mutable closures + module aliasing + benchmark suite, 2026-05-14)
884

985
🎯 **Three more architectural moves: closures gain shared mutable state, the module system gets namespaced imports, and OMC has its first benchmark suite.**

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ What this is **not**: a fast runtime, a production toolchain, a stable API, a de
5050
| Built-in test runner | `examples/test_runner.omc` | `fn test_*()` functions auto-discovered via `defined_functions()` and dispatched via `call(name, args)`. `assert_eq` / `assert_array_eq` etc. record failures in host-side state |
5151
| Mutable closures (Rc<RefCell> shared capture) | `examples/test_runner.omc` | Bank-account pattern: multiple closures from `make_account(100)` share the same `balance` binding; mutations propagate across all of them |
5252
| Module system with namespace aliasing | `examples/module_demo.omc` | `import "math_module.omc" as math` then `math.fib_up_to(100)``0,1,1,2,3,5,8,13,21,34,55,89`. Idempotent re-import; literal-path resolution |
53-
| Benchmark suite | `examples/benchmarks.omc` | Times `int_add` / `str_concat` / `arr_push` / `recursive fib(22)` / `is_fibonacci` etc. with per-op ns. Run with `OMC_VM=1` to compare against the bytecode VM |
53+
| Benchmark suite | `examples/benchmarks.omc` | Times `int_add` / `str_concat` / `arr_push` / `recursive fib(22)` / `is_fibonacci` etc. with per-op ns. Run with `OMC_VM=1` to compare against the bytecode VM. Direct-call variant shows the VM's 2.4× speedup on `recursive_fib`. |
54+
| Host-side self-healing pass (`OMC_HEAL=1`) | `examples/heal_pass_demo.omc` | Any OMC program benefits: harmonic-violation rewrites, typo correction with user-fn-preferred tiebreaker, literal `/0``safe_divide`, and arity auto-pad/truncate at call sites |
55+
| Closures on the bytecode VM | `examples/test_runner.omc` (run with `OMC_VM=1`) | Lambda expressions now compile under VM. Bank-account pattern produces identical output on tree-walk and VM. Test runner runs cleanly via `OMC_VM=1` |
5456

5557
Run any of these with the binary built from this repo:
5658

STDLIB.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ fn make_account(balance) {
329329
}
330330
```
331331

332-
VM caveat: lambdas execute via tree-walk, not the Rust bytecode VM. The VM's `OMC_VM=1` path bails with an error on `Expression::Lambda` because the bytecode VM has no captured-scope plumbing. Tree-walk works cleanly.
332+
**VM update (2026-05-14):** lambdas now compile on the Rust VM. `Op::Lambda(name)` creates a `Value::Function` at runtime with the current scope captured. Body execution still routes through tree-walk via `call_first_class_function`, so closures aren't VM-fast yet — but they no longer error under `OMC_VM=1`. The test runner runs cleanly via the VM now.
333333

334334
---
335335

examples/benchmarks.omc

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,42 @@ run_bench("is_fibonacci 0..N", bench_is_fibonacci, N_RESONANCE);
146146
run_bench("harmony_value 0..N", bench_harmony_value, N_RESONANCE);
147147
run_bench("recursive fib(N)", bench_recursive_fib, N_FIB);
148148

149+
# --- Direct-call variant -----------------------------------------------
150+
# The harness above dispatches via `call(fn, args)` — which routes user
151+
# fns through tree-walk semantics regardless of OMC_VM. To isolate the
152+
# VM hot path on direct Op::Call dispatch, we call the same fn bodies
153+
# inline here with hardcoded names. The numbers should diverge from the
154+
# reflective harness above when running with OMC_VM=1 — direct calls
155+
# get the bytecode-VM speedup; reflective dispatch doesn't.
156+
157+
println("");
158+
println(str_repeat("=", 70));
159+
println("Direct-call (no `call` indirection) — isolates VM hot path");
160+
println(str_repeat("=", 70));
161+
162+
fn time_direct(label, iters) {
163+
h start = now_ms();
164+
if label == "int_add" { bench_int_add(iters); }
165+
if label == "int_mul" { bench_int_mul(iters); }
166+
if label == "is_fibonacci" { bench_is_fibonacci(iters); }
167+
if label == "harmony_value" { bench_harmony_value(iters); }
168+
if label == "recursive_fib" { bench_recursive_fib(iters); }
169+
h elapsed_ms = now_ms() - start;
170+
h ns_per_op = elapsed_ms * 1000000 / iters;
171+
println(concat_many(
172+
str_pad_right(label, 32, " "),
173+
str_pad_left(to_string(iters), 8, " "), " iters ",
174+
str_pad_left(to_string(elapsed_ms), 6, " "), " ms ",
175+
str_pad_left(to_string(ns_per_op), 8, " "), " ns/op"
176+
));
177+
}
178+
179+
time_direct("int_add", N_INT_OPS);
180+
time_direct("int_mul", N_INT_OPS);
181+
time_direct("is_fibonacci", N_RESONANCE);
182+
time_direct("harmony_value", N_RESONANCE);
183+
time_direct("recursive_fib", N_FIB);
184+
149185
println(str_repeat("=", 70));
150186
println("Done. Run with OMC_VM=1 to compare against the Rust bytecode VM.");
151187
println(str_repeat("=", 70));

examples/heal_pass_demo.omc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# =============================================================================
2+
# OMC_HEAL — host-side self-healing pass demonstration
3+
# =============================================================================
4+
# Set OMC_HEAL=1 to enable. The healer walks the AST after parsing,
5+
# applies four classes of rewrites using Phase O's φ-math substrate,
6+
# prints diagnostics to stderr, and runs the healed program.
7+
#
8+
# Run without healing (errors out):
9+
# ./target/release/omnimcode-standalone examples/heal_pass_demo.omc
10+
#
11+
# Run with healing (executes to completion):
12+
# OMC_HEAL=1 ./target/release/omnimcode-standalone examples/heal_pass_demo.omc
13+
#
14+
# Set OMC_HEAL_QUIET=1 to suppress the diagnostic preamble; the
15+
# rewrites still happen, just silently.
16+
# =============================================================================
17+
18+
fn fib(n) {
19+
if n < 2 { return n; }
20+
return fib(n - 1) + fib(n - 2);
21+
}
22+
23+
# Harmonic violation — 145 is close to Fibonacci 144. Healed.
24+
h target = 145;
25+
26+
# Literal divide-by-zero. Healed to safe_divide(100, 0) = 100.
27+
h result = 100 / 0;
28+
println(result);
29+
30+
# Identifier typo — fbi is edit-distance 2 from fib. The healer
31+
# prefers user-defined fns over builtins as tiebreaker, so this
32+
# resolves to fib (not pi, which is also edit-distance 2).
33+
println(fbi(7));
34+
35+
# Arity mismatch (too few) — fib takes 1 arg, called with 0.
36+
# Healed to fib(0) by padding with a zero literal.
37+
println(fib());
38+
39+
# Arity mismatch (too many) — extras get truncated, harmonic
40+
# violations in the args get rewritten before truncation.
41+
# fib(10, 20, 30) → fib(8, 21, 34) → fib(8) = 21.
42+
println(fib(10, 20, 30));

omnimcode-core/src/bytecode.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ pub enum Op {
116116
/// as ArrSetNamed — required so the mutation propagates back through
117117
/// the VM scope instead of getting lost in vm_call_builtin's shim.
118118
SafeArrSetNamed(String),
119+
/// Closure creation. Pushes a Value::Function whose name is the
120+
/// String (resolves to a CompiledFunction in module.functions), and
121+
/// whose captured env is the current top scope frame (cloned Rc).
122+
/// Sibling closures created in the same scope share the captured
123+
/// env so mutations propagate, same as tree-walk.
124+
///
125+
/// Note: the body of the lambda is COMPILED as bytecode and stored
126+
/// in module.functions, but actual INVOCATION still routes through
127+
/// call_first_class_function → tree-walk semantics for the body.
128+
/// Fast bytecode-VM execution of closure bodies is future work.
129+
Lambda(String),
119130

120131
// Special harmonic operations (short-circuit to built-in semantics
121132
// without the call overhead — these are the hot ones).
@@ -162,6 +173,13 @@ pub struct CompiledFunction {
162173
pub struct Module {
163174
pub main: CompiledFunction,
164175
pub functions: std::collections::HashMap<String, CompiledFunction>,
176+
/// Lambda body ASTs collected during compilation. Each entry is
177+
/// (name, params, body_statements). Used by main.rs when running
178+
/// in VM mode — closure invocation routes through the interpreter's
179+
/// tree-walk path (call_first_class_function → invoke_user_function),
180+
/// which dispatches by name through `self.interp.functions`. We
181+
/// register these there before VM execution so reflection works.
182+
pub lambda_asts: Vec<(String, Vec<String>, Vec<crate::ast::Statement>)>,
165183
}
166184

167185
impl Default for Module {
@@ -177,6 +195,7 @@ impl Default for Module {
177195
call_cache: Vec::new(),
178196
},
179197
functions: std::collections::HashMap::new(),
198+
lambda_asts: Vec::new(),
180199
}
181200
}
182201
}

0 commit comments

Comments
 (0)