You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Mutable closures + module aliasing + benchmark suite
Track 1 — Mutable closures via Rc<RefCell> capture:
Closure capture went from snapshot-by-value to shared-reference.
Bank-account pattern works: multiple closures created in the
same scope share the same captured bindings; mutations propagate.
fn make_account(balance) {
h deposit = fn(amount) { balance = balance + amount; ... };
h withdraw = fn(amount) { balance = balance - amount; ... };
...
}
# deposit(50) then withdraw(30) → balance = 120, both see it
Architecture changes:
Value::Function.captured: HashMap → Rc<RefCell<HashMap>>
Interpreter.locals: Vec<HashMap> → Vec<Rc<RefCell<HashMap>>>
Lambda eval clones the current scope's Rc (siblings share)
New assign_var method walks outward looking for existing binding
Statement::Assignment now uses assign_var (not set_var)
call_first_class_function pushes captured env Rc as a scope frame
9 locals-access sites refactored to use the new Rc semantics
Counter pattern: works (3 → 4 with second closure independent).
Bank account pattern: works (100 → 150 → 120 → 120 across siblings).
Test runner still passes 5/6 (1 intentional failure unchanged).
Track 2 — Module aliasing (import "path" as alias):
The optional `as` clause was previously parsed but ignored.
Now wired through: aliased imports register their functions as
literal "alias.fname" keys in the function table.
import "examples/math_module.omc" as math;
println(arr_join(math.fib_up_to(100), ", "));
Module resolver gained literal-path support — absolute paths,
./relative paths, and .omc-suffixed names now resolve directly
without OMC_STDLIB_PATH. Short names still go through search
paths for stdlib imports.
Fixed an infinite-recursion bug while wiring this: call_function
now checks self.functions for the exact name BEFORE splitting at
`.`, so `call_function("math.fib") → call_module_function("math",
"fib") → call_function("math.fib")` doesn't loop.
Two example files:
examples/math_module.omc — reusable utility module
examples/module_demo.omc — usage demo
Track 3 — Benchmark suite (examples/benchmarks.omc):
OMC's first proper benchmark suite. Times int arithmetic,
string ops, array ops, harmonic primitives, and recursive fib
with now_ms() and reports per-op nanoseconds.
Tree-walk numbers (200K int_adds in 89ms = 445 ns/op,
recursive fib(22) in 53ms = 2.4ms/op).
Honest finding: OMC_VM=1 produces nearly-identical numbers on
this suite because benchmarks dispatch user fns via call(fn,
args), which routes through invoke_user_function (tree-walk).
The VM advantage applies to direct Op::Call paths, not
reflective dispatch. Useful signal for future VM work.
VM gains first-class-function-value fallback:
Op::LoadVar now falls back to checking module.functions and the
interpreter's function table when a name isn't a variable, so
arr_map(xs, bench_int_add) works under OMC_VM=1.
main.rs pre-registers user fn definitions into the VM's internal
interpreter (vm.interp_mut().register_user_functions(&statements))
before vm.run_module(...) so reflective dispatch resolves them.
Documentation:
STDLIB.md — closures section rewritten for mutable semantics;
counter and bank-account examples; VM caveat noted.
README — "What's proven right now" gains four rows (mutable
closures, modules, benchmark suite, plus the test runner
that was already there).
CHANGELOG — full track-by-track entry.
Regression: V.9b ✓✓✓ unchanged. H.5: 6/6 converge.
safe_keyword_host on both interpretation paths identical.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
🎯 **Three more architectural moves: closures gain shared mutable state, the module system gets namespaced imports, and OMC has its first benchmark suite.**
-`Interpreter.locals`: `Vec<HashMap>` → `Vec<Rc<RefCell<HashMap>>>`. Each scope frame is a shareable Rc.
32
+
- Lambda evaluation clones the Rc of the current scope frame (instead of taking a HashMap snapshot). Sibling closures created in the same enclosing call see the SAME underlying map; mutations propagate.
33
+
- New `assign_var` method on Interpreter: walks locals from inner to outer looking for an existing binding; if found, mutates in-place. `Statement::Assignment` now routes through `assign_var` instead of `set_var`. `h x = ...` (declaration) keeps using `set_var` to always create a fresh innermost binding.
34
+
-`call_first_class_function` pushes the captured env Rc as a scope frame BEFORE the args frame, so lookups via lexical chain hit the captured bindings naturally.
35
+
36
+
The single-closure case (counter pattern) and multi-closure-shared-state (bank account) both work. Refactor touched 9 scope-access sites in `interpreter.rs`. Verified end-to-end with `examples/test_runner.omc` (which uses lambdas internally) and a counter/bank-account smoke test.
37
+
38
+
#### Track 2 — Module aliasing (`import "path" as alias`)
39
+
40
+
`import` already parsed an optional `as` clause but the alias was ignored. Now it's wired through:
When an import has an alias, every function the module DEFINES gets renamed to `alias.fname` in the function table. Top-level statements still execute against the global namespace. Re-importing the same path is idempotent (deduped on path, not on alias).
49
+
50
+
The module resolver gained literal-path support — `import "/abs/path.omc"` and `import "./local.omc"` now work without `OMC_STDLIB_PATH` setup. Still falls back to search-path resolution for short names like `import core` or `import std/io`.
51
+
52
+
The dotted-call dispatch in `call_module_function` now checks for the full `module.fname` in the user function table BEFORE splitting at `.` and delegating. Otherwise we'd infinite-loop: `call_function("math.fib") → call_module_function("math", "fib") → call_function("math.fib") → …`. Fixed at the entry to `call_function` (check exact name before splitting).
53
+
54
+
Two new example files:
55
+
-`examples/math_module.omc` — a reusable utility module with `fib_up_to`, `cube_root`, `sum_range`, `euclid_gcd`.
56
+
-`examples/module_demo.omc` — demonstrates `import as` usage and idempotent re-import.
57
+
58
+
#### Track 3 — Benchmark suite (`examples/benchmarks.omc`)
59
+
60
+
OMC's first benchmark suite. Times common operations with `now_ms()` and reports per-operation nanoseconds. Run both ways:
**Honest finding** revealed by the benchmark: `OMC_VM=1` produces nearly-identical numbers to tree-walk on this suite. Reason: benchmarks dispatch their bodies via the new `call(fn, args)` primitive, which routes user-function calls through `invoke_user_function` (tree-walk semantics). The VM advantage applies to **direct**`Op::Call` dispatch, not to reflective `call(...)` dispatch.
85
+
86
+
That's exactly the kind of signal a benchmark suite should produce — concrete data about where the VM helps and where it doesn't. Future work could add a direct-call variant of the suite to isolate the VM hot path.
87
+
88
+
#### VM gains a first-class-function-value fallback
89
+
90
+
`Op::LoadVar` in the bytecode VM now falls back to checking `module.functions` AND the interpreter's function table when a name isn't a variable. This makes `arr_map(xs, bench_int_add)` work under `OMC_VM=1` — `bench_int_add` resolves as `Value::Function`. Tree-walk had this fallback already; the VM was missing it.
91
+
92
+
Also: `main.rs` now calls `vm.interp_mut().register_user_functions(&statements)` before `vm.run_module(...)`, pre-populating the interpreter's function table with user-defined fn bodies so reflective dispatch (`call(name, args)`) can resolve them at runtime.
93
+
94
+
#### Regression
95
+
96
+
V.9b: ✓✓✓. H.5: 6/6 demos converge. test_runner: 5/6 (1 intentional failure). safe_keyword_host on both tree-walk and OMC_VM=1: identical output. The 9-site `locals` refactor touched a lot of code but no surface broke.
97
+
7
98
### Added (Closures + harmonic_hash/diff/dedupe + test runner — 15 new builtins, 2026-05-14)
8
99
9
100
🎯 **Three more tracks land: closures over local scope, three new harmonic variants, and a built-in test runner.**
Copy file name to clipboardExpand all lines: README.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,6 +48,9 @@ What this is **not**: a fast runtime, a production toolchain, a stable API, a de
48
48
| OMNIcode harmonic variants — operations that USE the φ-math substrate |`examples/harmonic_variants.omc`|`harmonic_write_file` gates writes by resonance ≥ 0.5; `harmonic_sort` puts Fibonacci values first; `harmonic_split` chunks strings at φ-aligned word boundaries; `harmonic_partition` buckets by nearest attractor |
49
49
| Closures over local scope (snapshot capture) |`examples/test_runner.omc`|`fn make_adder(n) { return fn(x) { return x + n; }; }` — partial application, currying, captured-state patterns |
50
50
| 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 |
51
+
| 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 |
52
+
| 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 |
51
54
52
55
Run any of these with the binary built from this repo:
Copy file name to clipboardExpand all lines: STDLIB.md
+21-5Lines changed: 21 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -305,16 +305,32 @@ These take ordinary operations and route them through the φ-math substrate. Any
305
305
|`call(fn, args_arr)`|`function, array -> T`| Dispatch a function value (or function-name string) with an arbitrary argument list unpacked from an array. Lets the test runner invoke zero-arg tests; lets user code do dynamic-arity dispatch. |
306
306
|`defined_functions()`|`-> array<string>`| Sorted array of all user-defined function names. Auto-generated `__lambda_N` anonymous functions are excluded. Used by the test runner to discover `test_*` functions. |
307
307
308
-
Lambdas — `fn(params) { body }` expression form — capture the current local scope by VALUE (snapshot, not reference). Read-only closures over their environment. Mutable closures require shared refs and are future work.
308
+
Lambdas — `fn(params) { body }` expression form — capture the enclosing local scope by REFERENCE (shared `Rc<RefCell>`). Multiple closures created in the same scope share state, and assignments to captured names propagate. Read-and-write closures.
309
309
310
310
```omc
311
-
fn make_adder(n) {
312
-
return fn(x) { return x + n; };
311
+
fn make_counter() {
312
+
h n = 0;
313
+
return fn() {
314
+
n = n + 1;
315
+
return n;
316
+
};
317
+
}
318
+
h c = make_counter();
319
+
println(c()); # 1
320
+
println(c()); # 2
321
+
println(c()); # 3
322
+
323
+
# Multiple closures over shared state — bank account pattern:
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.
0 commit comments