Skip to content

Commit 1deae52

Browse files
Phase H.5.1: close the safe arr_set bytecode-VM gap
examples/self_healing_h5.omc — `safe arr_set(VAR, idx, val)` now works through the OMC bytecode VM, not just under tree-walk. The gap: H.5 emitted `CALL_BUILTIN safe_arr_set` for safe-wrapped array writes. The bytecode VM's CALL_BUILTIN handler routes through a synthetic-scope shim (vm_call_builtin → __vm_arg_N synthetic Variables) that copies the array argument, so the mutation lived in a temp scope and never propagated back to the caller's binding. The fix is the exact pattern V.7c used for ARR_SET_NAMED: take the variable name out of the value stack and put it on the opcode. Encoder change: - SAFE_EXPR branch's arr_set handler now detects bare-VAR first arg and emits SAFE_ARR_SET_NAMED varname (after pushing idx and val). Falls back to CALL_BUILTIN for non-VAR shapes (e.g. nested-array writes), which is no-mutation by design. Executor change: - New SAFE_ARR_SET_NAMED handler: pop val, pop raw_idx, look up arr in current scope by name. Compute healed index via nearest_fib (already in the file) and Euclidean mod by arr_len. arr_set on the local clone, scope_set writes back. Leaves the (mutated) array on the value stack as result. New demo 4b: `safe arr_set(buf, idx, val)` on a 5-element zero buffer with idx ∈ {0, 100, -1, 6}. Result: [55, 13, 0, 0, 34] — every write landed at the fold-and-mod position, every output has φ=1.000. Six demos, six convergences. The "known limit" note in the observation block is replaced with a description of how the gap was closed. The Rust VM doesn't need this fix because it already has ArrSetNamed; H.5's tree-walk path also already worked (it just didn't reach the bytecode VM). H.5.1 specifically closes the OMC-written-executor path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 0cc0f38 commit 1deae52

1 file changed

Lines changed: 81 additions & 17 deletions

File tree

examples/self_healing_h5.omc

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,21 @@ fn enc_expr(ast) {
811811
}
812812
if name == "arr_set" {
813813
if arr_len(args) == 3 {
814+
# H.5.1: when the array argument is a bare VAR, emit
815+
# SAFE_ARR_SET_NAMED so the mutation propagates back
816+
# through the bytecode VM's scope (same trick V.7c
817+
# used for ARR_SET_NAMED). Without this, CALL_BUILTIN
818+
# routes through a synthetic-scope shim that copies
819+
# the array argument and loses the write.
820+
h target = arr_get(args, 0);
821+
if arr_get(target, 0) == "VAR" {
822+
out = arr_concat(out, enc_expr(arr_get(args, 1))); # idx
823+
out = arr_concat(out, enc_expr(arr_get(args, 2))); # val
824+
arr_push(out, ["SAFE_ARR_SET_NAMED", arr_get(target, 1)]);
825+
return out;
826+
}
827+
# Fallback: not a bare VAR (e.g. nested array). Goes
828+
# through CALL_BUILTIN — works under tree-walk only.
814829
out = arr_concat(out, enc_expr(arr_get(args, 0)));
815830
out = arr_concat(out, enc_expr(arr_get(args, 1)));
816831
out = arr_concat(out, enc_expr(arr_get(args, 2)));
@@ -1244,6 +1259,31 @@ fn execute(prog) {
12441259
pc = pc + 1;
12451260
handled = 1;
12461261
}
1262+
if kind == "SAFE_ARR_SET_NAMED" {
1263+
# H.5.1: like ARR_SET_NAMED but fold-and-mod the index so
1264+
# an out-of-bounds write becomes an in-bounds write at the
1265+
# nearest Fibonacci-attractor position. The named-store
1266+
# form is required (vs CALL_BUILTIN safe_arr_set) because
1267+
# the bytecode VM's CALL_BUILTIN shim copies array args
1268+
# and loses the mutation.
1269+
h pr1 = pop1(stack); h val = arr_get(pr1, 0); stack = arr_get(pr1, 1);
1270+
h pr2 = pop1(stack); h raw_idx = arr_get(pr2, 0); stack = arr_get(pr2, 1);
1271+
h name = arr_get(op, 1);
1272+
h arr = scope_get(scope, name);
1273+
h len = arr_len(arr);
1274+
if len > 0 {
1275+
h folded = nearest_fib(raw_idx);
1276+
# Euclidean mod — OMC's % can return negatives for
1277+
# negative operands; this normalises into [0, len).
1278+
h healed = folded - (folded / len) * len;
1279+
if healed < 0 { healed = healed + len; }
1280+
arr_set(arr, healed, val);
1281+
scope = scope_set(scope, name, arr);
1282+
}
1283+
arr_push(stack, arr);
1284+
pc = pc + 1;
1285+
handled = 1;
1286+
}
12471287
if kind == "MAKE_ARR" {
12481288
h len = to_int(arr_get(op, 1));
12491289
h pr = pop_n_ordered(stack, len);
@@ -2246,12 +2286,10 @@ print(lookup(xs, 7));";
22462286
h outcome_3 = run_h3("Demo 3 (with safe): arr_get with various OOB indices",
22472287
GUARDED_ARRAY, 5);
22482288

2249-
# -------- Demo 4: safe arr_get inside a loop walking off the end -----------
2250-
# Walk indices 0 through 9 against a 5-element array. Without `safe`, the
2251-
# loop crashes at idx=5. With `safe`, every iteration produces a finite
2252-
# value via fold-and-mod. The wrap is deterministic, so the second pass
2253-
# (idx=5..9) traces a pattern through the array driven by the Fibonacci
2254-
# attractor structure.
2289+
# -------- Demo 4a: safe arr_get inside a loop walking off the end ----------
2290+
# Walk indices 0 through 9 against a 5-element array via safe arr_get.
2291+
# Every iteration produces a finite, attractor-landing value via fold-
2292+
# and-mod. Confirms reads compose cleanly via the bytecode VM.
22552293

22562294
h SAFE_WALK = "fn read(xs, idx) {
22572295
return safe arr_get(xs, idx);
@@ -2263,9 +2301,36 @@ while i < 10 {
22632301
i = i + 1;
22642302
}";
22652303

2266-
h outcome_4 = run_h3("Demo 4: safe arr_get walking off the end",
2304+
h outcome_4 = run_h3("Demo 4a: safe arr_get walking off the end",
22672305
SAFE_WALK, 5);
22682306

2307+
# -------- Demo 4b: safe arr_set with H.5.1 named-store op ------------------
2308+
# Treat a 5-element buffer as a circular write target. Each `safe arr_set`
2309+
# call folds the index onto the nearest Fibonacci attractor and modulos
2310+
# by arr_len, then mutates. The encoder emits SAFE_ARR_SET_NAMED (not
2311+
# CALL_BUILTIN safe_arr_set) when the first arg is a bare VAR, so the
2312+
# mutation propagates back through the bytecode VM's scope.
2313+
# idx=0 → fold to 0 → 1 (no-zero rule) → buf[1] = 11.
2314+
# But fold_to_fibonacci_const(0) returns 0; nearest_fib in OMC has
2315+
# the no-zero rule which makes it 1. So writes target index 1.
2316+
# idx=100 → fold to 89 → 89 % 5 = 4 → buf[4] = 22.
2317+
# idx=-1 → fold to -1 → fold inside nearest_fib gives -1; (-1 - (-1/5)*5) = -1; +5 = 4 → buf[4] = 33 (overwrites).
2318+
# idx=6 → fold to 5 → 5 % 5 = 0 → buf[0] = 55 (matches the 55 in original list, harmonic).
2319+
2320+
h SAFE_SET = "h buf = [0, 0, 0, 0, 0];
2321+
safe arr_set(buf, 0, 11);
2322+
safe arr_set(buf, 100, 22);
2323+
safe arr_set(buf, 0 - 1, 33);
2324+
safe arr_set(buf, 6, 55);
2325+
h i = 0;
2326+
while i < arr_len(buf) {
2327+
print(arr_get(buf, i));
2328+
i = i + 1;
2329+
}";
2330+
2331+
h outcome_4b = run_h3("Demo 4b: safe arr_set (H.5.1 named-store op)",
2332+
SAFE_SET, 5);
2333+
22692334
# -------- Demo 5: composed with H.4 in one program -------------------------
22702335
# Both `safe a/b` and `safe arr_get(a, i)` in the same function. The
22712336
# program survives both a singular divisor and an OOB index.
@@ -2288,8 +2353,9 @@ print("############################################################");
22882353
print(concat_many(" Demo 1 (H.4 regression): ", outcome_1));
22892354
print(concat_many(" Demo 2 (no safe arr_get): ", outcome_2));
22902355
print(concat_many(" Demo 3 (safe arr_get OOB): ", outcome_3));
2291-
print(concat_many(" Demo 4 (safe arr_get loop): ", outcome_4));
2292-
print(concat_many(" Demo 5 (H.4 + H.5 composed): ", outcome_5));
2356+
print(concat_many(" Demo 4a (safe arr_get loop): ", outcome_4));
2357+
print(concat_many(" Demo 4b (safe arr_set H.5.1): ", outcome_4b));
2358+
print(concat_many(" Demo 5 (H.4 + H.5 composed): ", outcome_5));
22932359
print("");
22942360
print("# Observations");
22952361
print("#");
@@ -2311,14 +2377,12 @@ print("# in call_builtin, extend the SAFE_EXPR rewrite to recognize the");
23112377
print("# call shape. The substrate carries the heavy semantics; H.5 is");
23122378
print("# structural composition, not new invention.");
23132379
print("#");
2314-
print("# - Known limit: `safe arr_set(VAR, ...)` works under tree-walk but");
2315-
print("# not via the OMC bytecode VM. The bytecode VM routes CALL_BUILTIN");
2316-
print("# through a synthetic-scope shim that copies array arguments,");
2317-
print("# losing the mutation back to the caller's variable. The Rust VM");
2318-
print("# solved this with ARR_SET_NAMED opcodes; H.5.1 would add an");
2319-
print("# SAFE_ARR_SET_NAMED variant. Reads (safe arr_get) compose cleanly");
2320-
print("# through either path because they return a value rather than");
2321-
print("# mutating a binding.");
2380+
print("# - H.5.1 closed the named-write gap. `safe arr_set(VAR, idx, val)`");
2381+
print("# now emits SAFE_ARR_SET_NAMED (not CALL_BUILTIN), bypassing the");
2382+
print("# synthetic-scope shim that lost the mutation. Demo 4b verifies");
2383+
print("# this end-to-end through the OMC bytecode VM. The pattern is the");
2384+
print("# exact same one V.7c used for ARR_SET_NAMED: take the variable");
2385+
print("# name out of the value stack and put it on the opcode.");
23222386
print("############################################################");
23232387

23242388

0 commit comments

Comments
 (0)