Skip to content

Commit 2a4321c

Browse files
Iterative heal + heal-retry + VM-native reflective dispatch + --check/--fmt
Four tracks; the autofixer "snowballs" into a real toolchain feature. Track 1 — Iterative heal pass: heal_ast_until_fixpoint(stmts, max_iter) runs the heal pass repeatedly until convergence ("converged", zero diagnostics in the final pass), "stuck" (same count two iterations running), or "exhausted" (hit max_iter). OMC_HEAL=1 now uses iterative with max_iter=5. Catches cases where one fix exposes another. Track 2 — Heal-on-runtime-error (OMC_HEAL_RETRY=1): Catches an error from interpreter.execute, runs heal_ast on a fresh copy of the AST, retries once. Combines static (compile-time) and dynamic (run-time) discovery. Demo: a program with `print(fbi(7));` errors normally; with OMC_HEAL_RETRY=1 it catches the error, heals fbi → fib and 7 → 8 (close-miss Fibonacci), and re-runs to produce 21. Track 3 — VM-native reflective dispatch for `call(fn, args)`: Intercepts Op::Call("call") in vm.rs BEFORE dispatching to vm_call_builtin. Extracts the function and args, checks if the target is in module.functions, dispatches via self.run_function with captured env attached. Falls through to tree-walk for non-VM-compiled targets. Result: recursive_fib(22) reflective dispatch drops from 2.4 ms/op (via tree-walk) to 1.09 ms/op — a 2.2x speedup on reflective calls. Direct and reflective produce identical numbers now. The test runner runs at full VM speed under OMC_VM=1. Track 4 — CLI flags (--check, --fmt, --help): --check FILE runs heal pass, prints diagnostics, exits. For CI / lint workflows. --fmt FILE pretty-prints AST as canonical OMC source. Lossy on whitespace/comments; canonical indentation and parens. --help lists all flags and OMC_* environment variables. New omnimcode-core/src/formatter.rs (300 lines) handles the AST → source pretty-print. Indents 4 spaces, parenthesizes every BIN op (matches V.4's "no precedence ambiguity" convention), re-encodes string escapes. Bug fix along the way: lambda namespace collision. Compile-time lambda counter (LAMBDA_SEQ in compiler.rs) and tree-walk lambda counter (self.lambda_counter) both produced __lambda_N starting from 0. Nested fns dispatch via tree-walk; a runtime-created lambda would overwrite a VM-compiled one with the same number, corrupting the global function table. Cross-test contamination manifested as "Undefined variable: n" after test_closures ran — its captured n leaked into a sibling test. Fix: tree-walk lambdas use prefix __rt_lambda_N, distinct from the compiler's __lambda_N pool. defined_functions() filters both prefixes. Nested fn registration: register_user_functions now walks recursively into fn bodies, if/elif/else branches, while/for bodies — registering every Statement::FunctionDef. Required for nested fns to be reachable when the VM dispatches the outer fn (which doesn't compile nested defs into bytecode). Regression: V.9b ✓✓✓. H.5 6/6. test_runner 5/6 on BOTH tree-walk and OMC_VM=1. safe_keyword_host, module_demo, heal_pass_demo, mutable_closure_test, benchmarks all good. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 40627f2 commit 2a4321c

7 files changed

Lines changed: 657 additions & 43 deletions

File tree

CHANGELOG.md

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

55
## [Unreleased]
66

7+
### Added (Iterative heal + heal-retry + VM-native reflective dispatch + --check / --fmt CLI, 2026-05-14)
8+
9+
🎯 **Four tracks: the autofixer snowballs to a fixpoint, catches runtime errors, the VM speedup applies to reflective dispatch, and OMC gets `--check` and `--fmt` CLI flags.**
10+
11+
#### Track 1 — Iterative heal pass
12+
13+
`heal_ast_until_fixpoint(stmts, max_iter)` runs `heal_ast` repeatedly until convergence, "stuck" (same diagnostic count two iterations in a row), or "exhausted" (hit max_iter). `OMC_HEAL=1` now uses iterative with `max_iter=5`. Catches cases where one fix exposes another — e.g. a typo correction whose new arg list also has harmonic violations.
14+
15+
#### Track 2 — Heal-on-runtime-error (`OMC_HEAL_RETRY=1`)
16+
17+
When set, catches the error from `interpreter.execute(stmts)`, runs `heal_ast_until_fixpoint` on a fresh copy of the AST, and retries once. Combines static discovery (catches what compile-time analysis can see) with dynamic discovery (catches what only fires at runtime).
18+
19+
Demo: a program that calls undefined `fbi(7)` errors normally; with `OMC_HEAL_RETRY=1` it catches the `Undefined function: fbi`, heals to `fib(8)`, and re-runs to produce `21`.
20+
21+
The two heal modes compose — you can set both `OMC_HEAL=1` and `OMC_HEAL_RETRY=1` for "pre-heal AST + retry if something still goes wrong."
22+
23+
#### Track 3 — VM-native dispatch for `call(fn, args)`
24+
25+
The reflective dispatch path `call(fn, args_array)` previously routed through `vm_call_builtin → call_function → invoke_user_function` (tree-walk), losing the bytecode-VM hot-path advantage. Now intercepted at the `Op::Call("call")` site:
26+
27+
```
28+
if name == "call" && argvals.len() == 2 {
29+
// Extract fn name + args array, check if target is VM-compiled,
30+
// dispatch via self.run_function with captured env attached.
31+
// Falls through to tree-walk for non-VM-compiled targets.
32+
}
33+
```
34+
35+
**Real speedup:** `recursive_fib(22)` invoked via `call(bench_recursive_fib, [22])` drops from 2.4 ms (via tree-walk) to 1.09 ms (VM-native) — a **2.2× speedup on reflective dispatch**. The test runner — which dispatches every test via `call(test_name, args)` — now runs at full bytecode-VM speed under `OMC_VM=1`.
36+
37+
Verified end-to-end: `examples/test_runner.omc` runs cleanly via `OMC_VM=1`, 5/6 with the expected intentional failure.
38+
39+
#### Track 4 — CLI flags `--check`, `--fmt`, `--help`
40+
41+
OMC gets real toolchain integration:
42+
43+
- **`--check FILE`** runs the heal pass and reports diagnostics without executing. Exits 0 if clean, 1 with diagnostics. Useful for CI / lint workflows.
44+
- **`--fmt FILE`** pretty-prints the AST back to canonical OMC source — indented, BIN operations parenthesized for unambiguous re-parse, escape sequences re-encoded. Strips whitespace and comments (lossy on those). New `omnimcode-core/src/formatter.rs` module.
45+
- **`--help`** lists all flags and environment variables.
46+
47+
```
48+
$ ./target/release/omnimcode-standalone --check examples/heal_pass_demo.omc
49+
examples/heal_pass_demo.omc: 8 diagnostic(s) over 1 iteration(s) (converged)
50+
harmonic: 145 not Fibonacci → 144 (|Δ|=1)
51+
divide-by-zero: rewriting to safe_divide(...)
52+
...
53+
```
54+
55+
```
56+
$ cat /tmp/ugly.omc
57+
fn fib(n){if n<2{return n;}return fib(n-1)+fib(n-2);}
58+
h x=89;print(fib(x));
59+
60+
$ ./target/release/omnimcode-standalone --fmt /tmp/ugly.omc
61+
fn fib(n) {
62+
if (n < 2) {
63+
return n;
64+
}
65+
return (fib((n - 1)) + fib((n - 2)));
66+
}
67+
h x = 89;
68+
print(fib(x));
69+
```
70+
71+
#### One bug found and fixed along the way: lambda namespace collision
72+
73+
The compile-time lambda counter (`LAMBDA_SEQ` in `compiler.rs`) and the tree-walk lambda counter (`self.lambda_counter`) both produced `__lambda_N` names starting from 0. Nested fns dispatch via tree-walk (not VM-native), so a lambda created inside a nested fn at runtime would overwrite a VM-compiled lambda with the same number, corrupting the global function table. The cross-test contamination would manifest as `Undefined variable: n` after test_closures had run (its captured `n` env leaked into a sibling test).
74+
75+
Fix: tree-walk-time lambdas now use prefix `__rt_lambda_N`, distinct from the compiler's `__lambda_N` pool. `defined_functions()` filters both prefixes.
76+
77+
#### Nested fn registration
78+
79+
`register_user_functions` now walks recursively into fn bodies, if/elif/else branches, while bodies, and for-loop bodies — registering EVERY `Statement::FunctionDef` into the interpreter's function table. Required because `fn make_adder()` inside `fn test_closures()` would otherwise be unreachable when the test runner dispatches `test_closures` and that body calls `make_adder()` directly.
80+
81+
#### Regression
82+
83+
V.9b ✓✓✓ unchanged. H.5: 6/6 demos converge. test_runner: 5/6 on BOTH tree-walk and `OMC_VM=1`. `safe_keyword_host`, `module_demo`, `mutable_closure_test`, `heal_pass_demo`, `benchmarks` all produce expected output. No surface broken.
84+
785
### Added (Host-side autofixer + VM closures + direct-call benchmarks, 2026-05-14)
886

987
🎯 **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.**

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ What this is **not**: a fast runtime, a production toolchain, a stable API, a de
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 |
5353
| 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 |
54+
| Host-side self-healing pass (`OMC_HEAL=1`) | `examples/heal_pass_demo.omc` | Iterative until fixpoint. 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+
| Heal-on-runtime-error (`OMC_HEAL_RETRY=1`) | any program — catches `Undefined function: fbi` etc. | Execution errors trigger one round of healing + retry. Combines static and dynamic discovery into a single auto-recover pass |
56+
| CLI flags `--check` / `--fmt` | `omnimcode-standalone --check examples/heal_pass_demo.omc` | `--check` runs the heal pass and reports diagnostics without executing. `--fmt` pretty-prints AST as canonical OMC source. `--help` lists all environment variables |
57+
| VM-native reflective dispatch | `OMC_VM=1` on `examples/benchmarks.omc` | `call(fn, args)` intercepted at `Op::Call("call")` site; targets in `module.functions` dispatch via `run_function`. Reflective and direct calls now produce identical numbers. Test runner runs cleanly under `OMC_VM=1` |
5558
| 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` |
5659

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

omnimcode-core/src/formatter.rs

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// omnimcode-core/src/formatter.rs — Canonical AST → OMC source emitter.
2+
//
3+
// Mirrors the V.4 pretty-printer from examples/self_healing_h5.omc but
4+
// operates on the host AST (not the nested-array AST used inside the
5+
// OMC-written self-hosting demos).
6+
//
7+
// Output is canonical, not byte-identical to the input. Whitespace,
8+
// comments, and original paren style are dropped. The emitter always
9+
// wraps BIN operations in parens to avoid precedence ambiguity — same
10+
// trade as V.4 ("the round-trip rule is no precedence ambiguity, not
11+
// minimal parens").
12+
//
13+
// Used by `--fmt` in main.rs.
14+
15+
use crate::ast::*;
16+
17+
const INDENT: &str = " ";
18+
19+
pub fn format_program(stmts: &[Statement]) -> String {
20+
let mut out = String::new();
21+
for s in stmts {
22+
format_stmt(s, 0, &mut out);
23+
}
24+
out
25+
}
26+
27+
fn indent_to(level: usize, out: &mut String) {
28+
for _ in 0..level {
29+
out.push_str(INDENT);
30+
}
31+
}
32+
33+
fn format_stmt(stmt: &Statement, level: usize, out: &mut String) {
34+
indent_to(level, out);
35+
match stmt {
36+
Statement::Print(e) => {
37+
out.push_str("print(");
38+
format_expr(e, out);
39+
out.push_str(");\n");
40+
}
41+
Statement::Expression(e) => {
42+
format_expr(e, out);
43+
out.push_str(";\n");
44+
}
45+
Statement::VarDecl { name, value, .. } => {
46+
out.push_str("h ");
47+
out.push_str(name);
48+
out.push_str(" = ");
49+
format_expr(value, out);
50+
out.push_str(";\n");
51+
}
52+
Statement::Parameter { name, value } => {
53+
out.push_str("h ");
54+
out.push_str(name);
55+
out.push_str(" = ");
56+
format_expr(value, out);
57+
out.push_str(";\n");
58+
}
59+
Statement::Assignment { name, value } => {
60+
out.push_str(name);
61+
out.push_str(" = ");
62+
format_expr(value, out);
63+
out.push_str(";\n");
64+
}
65+
Statement::IndexAssignment { name, index, value } => {
66+
out.push_str(name);
67+
out.push('[');
68+
format_expr(index, out);
69+
out.push_str("] = ");
70+
format_expr(value, out);
71+
out.push_str(";\n");
72+
}
73+
Statement::If { condition, then_body, elif_parts, else_body } => {
74+
out.push_str("if ");
75+
format_expr(condition, out);
76+
out.push_str(" {\n");
77+
for s in then_body {
78+
format_stmt(s, level + 1, out);
79+
}
80+
indent_to(level, out);
81+
out.push('}');
82+
for (econd, ebody) in elif_parts {
83+
out.push_str(" else if ");
84+
format_expr(econd, out);
85+
out.push_str(" {\n");
86+
for s in ebody {
87+
format_stmt(s, level + 1, out);
88+
}
89+
indent_to(level, out);
90+
out.push('}');
91+
}
92+
if let Some(body) = else_body {
93+
out.push_str(" else {\n");
94+
for s in body {
95+
format_stmt(s, level + 1, out);
96+
}
97+
indent_to(level, out);
98+
out.push('}');
99+
}
100+
out.push('\n');
101+
}
102+
Statement::While { condition, body } => {
103+
out.push_str("while ");
104+
format_expr(condition, out);
105+
out.push_str(" {\n");
106+
for s in body {
107+
format_stmt(s, level + 1, out);
108+
}
109+
indent_to(level, out);
110+
out.push_str("}\n");
111+
}
112+
Statement::For { var, iterable, body } => {
113+
out.push_str("for ");
114+
out.push_str(var);
115+
out.push_str(" in ");
116+
match iterable {
117+
ForIterable::Range { start, end } => {
118+
out.push_str("range(");
119+
format_expr(start, out);
120+
out.push_str(", ");
121+
format_expr(end, out);
122+
out.push(')');
123+
}
124+
ForIterable::Expr(e) => format_expr(e, out),
125+
}
126+
out.push_str(" {\n");
127+
for s in body {
128+
format_stmt(s, level + 1, out);
129+
}
130+
indent_to(level, out);
131+
out.push_str("}\n");
132+
}
133+
Statement::FunctionDef { name, params, body, return_type, .. } => {
134+
out.push_str("fn ");
135+
out.push_str(name);
136+
out.push('(');
137+
for (i, p) in params.iter().enumerate() {
138+
if i > 0 { out.push_str(", "); }
139+
out.push_str(p);
140+
}
141+
out.push(')');
142+
if let Some(rt) = return_type {
143+
out.push_str(" -> ");
144+
out.push_str(rt);
145+
}
146+
out.push_str(" {\n");
147+
for s in body {
148+
format_stmt(s, level + 1, out);
149+
}
150+
indent_to(level, out);
151+
out.push_str("}\n");
152+
}
153+
Statement::Return(opt) => {
154+
out.push_str("return");
155+
if let Some(e) = opt {
156+
out.push(' ');
157+
format_expr(e, out);
158+
}
159+
out.push_str(";\n");
160+
}
161+
Statement::Break => out.push_str("break;\n"),
162+
Statement::Continue => out.push_str("continue;\n"),
163+
Statement::Import { module, alias } => {
164+
out.push_str("import \"");
165+
out.push_str(module);
166+
out.push('"');
167+
if let Some(a) = alias {
168+
out.push_str(" as ");
169+
out.push_str(a);
170+
}
171+
out.push_str(";\n");
172+
}
173+
}
174+
}
175+
176+
fn format_expr(expr: &Expression, out: &mut String) {
177+
match expr {
178+
Expression::Number(n) => out.push_str(&n.to_string()),
179+
Expression::Float(f) => {
180+
// Keep the decimal point so re-parse doesn't collapse to int.
181+
let s = format!("{}", f);
182+
if s.contains('.') || s.contains('e') || s.contains('E') {
183+
out.push_str(&s);
184+
} else {
185+
out.push_str(&s);
186+
out.push_str(".0");
187+
}
188+
}
189+
Expression::String(s) => {
190+
out.push('"');
191+
for c in s.chars() {
192+
match c {
193+
'\\' => out.push_str("\\\\"),
194+
'"' => out.push_str("\\\""),
195+
'\n' => out.push_str("\\n"),
196+
'\t' => out.push_str("\\t"),
197+
'\r' => out.push_str("\\r"),
198+
_ => out.push(c),
199+
}
200+
}
201+
out.push('"');
202+
}
203+
Expression::Boolean(b) => out.push_str(if *b { "true" } else { "false" }),
204+
Expression::Variable(name) => out.push_str(name),
205+
Expression::Index { name, index } => {
206+
out.push_str(name);
207+
out.push('[');
208+
format_expr(index, out);
209+
out.push(']');
210+
}
211+
Expression::Array(items) => {
212+
out.push('[');
213+
for (i, e) in items.iter().enumerate() {
214+
if i > 0 { out.push_str(", "); }
215+
format_expr(e, out);
216+
}
217+
out.push(']');
218+
}
219+
Expression::Add(l, r) => format_binop(l, "+", r, out),
220+
Expression::Sub(l, r) => format_binop(l, "-", r, out),
221+
Expression::Mul(l, r) => format_binop(l, "*", r, out),
222+
Expression::Div(l, r) => format_binop(l, "/", r, out),
223+
Expression::Mod(l, r) => format_binop(l, "%", r, out),
224+
Expression::Eq(l, r) => format_binop(l, "==", r, out),
225+
Expression::Ne(l, r) => format_binop(l, "!=", r, out),
226+
Expression::Lt(l, r) => format_binop(l, "<", r, out),
227+
Expression::Le(l, r) => format_binop(l, "<=", r, out),
228+
Expression::Gt(l, r) => format_binop(l, ">", r, out),
229+
Expression::Ge(l, r) => format_binop(l, ">=", r, out),
230+
Expression::And(l, r) => format_binop(l, "and", r, out),
231+
Expression::Or(l, r) => format_binop(l, "or", r, out),
232+
Expression::Not(e) => {
233+
out.push_str("not ");
234+
format_expr(e, out);
235+
}
236+
Expression::BitAnd(l, r) => format_binop(l, "&", r, out),
237+
Expression::BitOr(l, r) => format_binop(l, "|", r, out),
238+
Expression::BitXor(l, r) => format_binop(l, "^", r, out),
239+
Expression::BitNot(e) => {
240+
out.push('~');
241+
format_expr(e, out);
242+
}
243+
Expression::Shl(l, r) => format_binop(l, "<<", r, out),
244+
Expression::Shr(l, r) => format_binop(l, ">>", r, out),
245+
Expression::Call { name, args } => {
246+
out.push_str(name);
247+
out.push('(');
248+
for (i, a) in args.iter().enumerate() {
249+
if i > 0 { out.push_str(", "); }
250+
format_expr(a, out);
251+
}
252+
out.push(')');
253+
}
254+
Expression::Resonance(e) => { out.push_str("res("); format_expr(e, out); out.push(')'); }
255+
Expression::Fold(e) => { out.push_str("fold("); format_expr(e, out); out.push(')'); }
256+
Expression::Safe(inner) => {
257+
out.push_str("safe ");
258+
format_expr(inner, out);
259+
}
260+
Expression::Lambda { params, body } => {
261+
out.push_str("fn(");
262+
for (i, p) in params.iter().enumerate() {
263+
if i > 0 { out.push_str(", "); }
264+
out.push_str(p);
265+
}
266+
out.push_str(") {\n");
267+
for s in body {
268+
format_stmt(s, 1, out);
269+
}
270+
out.push('}');
271+
}
272+
}
273+
}
274+
275+
fn format_binop(l: &Expression, op: &str, r: &Expression, out: &mut String) {
276+
out.push('(');
277+
format_expr(l, out);
278+
out.push(' ');
279+
out.push_str(op);
280+
out.push(' ');
281+
format_expr(r, out);
282+
out.push(')');
283+
}

0 commit comments

Comments
 (0)