Skip to content

Commit 1c7c884

Browse files
Phase H.5.2: Op::SafeArrSetNamed — close the last safe keyword gap
The host-level `safe` keyword shipped yesterday (commit 6e1b5f4) worked correctly through tree-walk but `safe arr_set(VAR, ...)` compiled to bytecode lost the mutation when run through the Rust VM — same shape gap V.7c originally closed for plain arr_set with Op::ArrSetNamed. The fix is structurally identical: take the variable name off the value stack and put it on the opcode itself, bypassing vm_call_builtin's synthetic-arg shim that copies array arguments. bytecode.rs: New Op::SafeArrSetNamed(String) variant. vm.rs: Dispatch handler — pop val, pop raw_idx, fold raw_idx onto nearest Fibonacci attractor via the new pub(crate) interpreter::fold_to_fibonacci_const, Euclidean-mod by arr_len, mutate, write back. Empty arrays silently drop the write (total semantics). compiler.rs: Safe(Call("arr_set", [Variable, idx, val])) emits SafeArrSetNamed; non-Variable first arg falls through to Op::Call("safe_arr_set", 3) which still loses mutation (same semantics as plain arr_set on non-VAR). disasm.rs: Disassembly format for the new op. interpreter.rs: fold_to_fibonacci_const promoted to pub(crate) so vm.rs can call it. Verified end-to-end: examples/safe_keyword_host.omc produces identical output under tree-walk and OMC_VM=1. The Rust VM disassembly shows `0027: SAFE_ARR_SET_NAMED xs` for the `safe arr_set(xs, 999, 99)` call — the mutation lands at xs[fold(999) % 3] = xs[1], propagating through VM scope correctly. Phase H is now end-to-end clean on BOTH interpretation paths. Self-healing semantics work the same way whether you're tree- walking or compiling-and-running, for every documented shape: safe divide, safe arr_get, safe arr_set. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent dc16b10 commit 1c7c884

5 files changed

Lines changed: 50 additions & 4 deletions

File tree

omnimcode-core/src/bytecode.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ pub enum Op {
109109
/// Mutating array store: pop value, pop index, store at named array's
110110
/// index. Same rationale as ArrPushNamed.
111111
ArrSetNamed(String),
112+
/// H.5.2: self-healing mutating array store. Pop value, pop raw_idx,
113+
/// fold raw_idx onto the nearest Fibonacci attractor, Euclidean-mod by
114+
/// arr_len, then store at the healed index. Out-of-bounds writes
115+
/// become attractor-landing in-bounds writes. Same name-on-opcode trick
116+
/// as ArrSetNamed — required so the mutation propagates back through
117+
/// the VM scope instead of getting lost in vm_call_builtin's shim.
118+
SafeArrSetNamed(String),
112119

113120
// Special harmonic operations (short-circuit to built-in semantics
114121
// without the call overhead — these are the hot ones).

omnimcode-core/src/compiler.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,22 @@ impl Compiler {
437437
self.emit(Op::Call("safe_arr_get".to_string(), 2));
438438
}
439439
Expression::Call { name, args } if name == "arr_set" && args.len() == 3 => {
440-
for arg in args {
441-
self.compile_expr(arg)?;
440+
// H.5.2: bare-VAR first arg → emit SafeArrSetNamed
441+
// so the mutation propagates back through VM scope.
442+
// Non-VAR shapes (e.g. nested array) fall through
443+
// to the synthetic-arg call shim, which loses the
444+
// mutation (same semantics as plain arr_set on a
445+
// non-VAR).
446+
if let Expression::Variable(arr_name) = &args[0] {
447+
self.compile_expr(&args[1])?; // index
448+
self.compile_expr(&args[2])?; // value
449+
self.emit(Op::SafeArrSetNamed(arr_name.clone()));
450+
} else {
451+
for arg in args {
452+
self.compile_expr(arg)?;
453+
}
454+
self.emit(Op::Call("safe_arr_set".to_string(), 3));
442455
}
443-
self.emit(Op::Call("safe_arr_set".to_string(), 3));
444456
}
445457
_ => self.compile_expr(inner)?,
446458
}

omnimcode-core/src/disasm.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ fn op_mnemonic(op: &Op, ip: usize, constants: &[Const]) -> String {
7575
Op::ArrayIndexAssign(name) => format!("ARRAY_INDEX_ASSIGN {}", name),
7676
Op::ArrPushNamed(name) => format!("ARR_PUSH_NAMED {}", name),
7777
Op::ArrSetNamed(name) => format!("ARR_SET_NAMED {}", name),
78+
Op::SafeArrSetNamed(name) => format!("SAFE_ARR_SET_NAMED {}", name),
7879

7980
Op::Resonance => "RESONANCE".to_string(),
8081
Op::Fold1 => "FOLD".to_string(),

omnimcode-core/src/interpreter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ fn values_equal(a: &Value, b: &Value) -> bool {
18431843

18441844
// Free function reused by quantize / quantization_ratio / mean_omni_weight.
18451845
// Snap |n| to the nearest Fibonacci attractor, preserving sign.
1846-
fn fold_to_fibonacci_const(n: i64) -> i64 {
1846+
pub(crate) fn fold_to_fibonacci_const(n: i64) -> i64 {
18471847
let fibs: [i64; 15] = [0, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610];
18481848
let abs_val = n.abs();
18491849
let mut nearest = fibs[0];

omnimcode-core/src/vm.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,32 @@ impl Vm {
289289
));
290290
}
291291
}
292+
Op::SafeArrSetNamed(name) => {
293+
let val = stack.pop().ok_or("stack underflow")?;
294+
let raw_idx = stack.pop().ok_or("stack underflow")?.to_int();
295+
if let Some(Value::Array(mut a)) = self.interp.vm_get_var(name) {
296+
let len = a.items.len();
297+
if len > 0 {
298+
// Fold onto nearest Fibonacci attractor, then
299+
// Euclidean mod by len.
300+
let folded = crate::interpreter::fold_to_fibonacci_const(raw_idx);
301+
let len_i = len as i64;
302+
let mut healed = folded % len_i;
303+
if healed < 0 {
304+
healed += len_i;
305+
}
306+
a.items[healed as usize] = val;
307+
self.interp.vm_set_local(name, Value::Array(a));
308+
}
309+
// Empty arrays: silently drop the write (total
310+
// semantics — never errors).
311+
} else {
312+
return Err(format!(
313+
"SafeArrSetNamed: {} is not an array variable",
314+
name
315+
));
316+
}
317+
}
292318
Op::ArrayIndexAssign(name) => {
293319
let idx = stack.pop().ok_or("stack underflow")?.to_int() as usize;
294320
let val = stack.pop().ok_or("stack underflow")?;

0 commit comments

Comments
 (0)