Skip to content

Commit 89c8694

Browse files
Path D: array writes (ArrSetNamed, ArrayIndexAssign) + cross-fn float
Adds mutable array writes to the dual-band JIT. Combined with Path A.4's read support, JIT'd OMC fns can now build, fill, and read arrays of arbitrary content within a single fn call — unlocking the build-then-process pattern that most numerical algorithms use (including the harmonic libraries). What's new: - Op::ArrSetNamed(name) handler: optimized form the compiler emits for `arr_set(name, idx, val)`. Pops value+index, looks up the named array slot, GEPs slot+1 (skip length prefix), stores value. Pushes a placeholder (0) on stack so the trailing Pop the compiler emits doesn't underflow. - Op::ArrayIndexAssign(name) handler: same semantics, used for `name[idx] = val` syntax. No placeholder push (statement form). - New helper emit_array_set_named consolidates both. Loads the array's pointer from the named slot's <2 x i64>, extracts α (the i64-bit-pattern pointer), inttoptr, GEP, store value's α. Beta semantics on writes: the value's β is discarded. Arrays hold scalar α only; reads via ArrayIndex splat back into matched (α, α) bands. This is a deliberate MVP choice — true β tracking through arrays would need parallel storage or a wider element type. The "harmonic array" type stays open as future work. Tests added (3 total): - jit_array_write_with_arr_set: build squares array via arr_set in a loop, verify a known slot - jit_array_write_then_sum: build squares, then sum them. Verifies sum_of_squares(10) = 285, sum_of_squares(5) = 30 - cross_fn_float_passing: documents the structural cross-fn-float capability AND its limitation: callee with untyped params emits Op::Add (int) on the float bit-pattern, producing wrong values. Cross-fn float math today requires explicit conversion at the fn boundary via to_int/to_float, OR statically-typed parameters in the OMC compiler (which is a separate compiler-side task). What's still NOT covered: - Op::ArrPushNamed (dynamic resize): our arrays are stack alloca with fixed length-at-NewArray-time. Push would need malloc/realloc or pre-allocated capacity. Out of scope for this MVP. - Op::SafeArrSetNamed (H.5.2 self-healing variant): folds the index to nearest Fibonacci attractor and Euclidean-mods by length. Unused in the harmonic libs we want to JIT, so deferred. - Cross-fn float passing where callee untyped: same compiler-side story as Path A.2's missing DivFloat — requires the bytecode compiler to emit AddFloat/SubFloat/MulFloat when call-site type info is available. Workspace: 41 codegen tests pass (1 IR + 4 cross-fn + 6 arrays + 5 dual-band + 5 dispatch + 4 floats + 3 harmony + 5 phi_shadow + 8 scalar). Smoke + harmonic-lib + 149 core tests still green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8db4dc2 commit 89c8694

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

omnimcode-codegen/src/dual_band.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,31 @@ impl<'ctx, 'a> DualBandLowerer<'ctx, 'a> {
392392
let val = self.emit_array_index(arr_v, idx_v, i)?;
393393
stack.push(self.splat(val, "aidx_v")?);
394394
}
395+
// Path D: array writes. ArrSetNamed(name) is the
396+
// optimized form the compiler emits for
397+
// `arr_set(name, idx, val)` where `name` is a literal
398+
// variable. Pops value, pops index, looks up the
399+
// array via the named slot, GEPs slot+1, stores.
400+
// Pushes a placeholder (0) so the trailing Pop the
401+
// compiler emits doesn't underflow — the OMC builtin
402+
// arr_set returns null in tree-walk.
403+
Op::ArrSetNamed(name) => {
404+
let val_v = self.pop(&mut stack, i, "ArrSetNamed value")?;
405+
let idx_v = self.pop(&mut stack, i, "ArrSetNamed idx")?;
406+
self.emit_array_set_named(name, idx_v, val_v, i)?;
407+
stack.push(self.splat(i64_type.const_int(0, false), "asn_ret")?);
408+
}
409+
// ArrayIndexAssign(name): the compiler's emit for
410+
// `name[idx] = val` syntax. Same semantics as
411+
// ArrSetNamed but the bytecode form is value-then-
412+
// index-then-op (different stack discipline).
413+
// Doesn't push a placeholder (the AST form is a
414+
// statement, not an expression).
415+
Op::ArrayIndexAssign(name) => {
416+
let val_v = self.pop(&mut stack, i, "ArrayIndexAssign value")?;
417+
let idx_v = self.pop(&mut stack, i, "ArrayIndexAssign idx")?;
418+
self.emit_array_set_named(name, idx_v, val_v, i)?;
419+
}
395420

396421
Op::Eq => self.cmp_vec(&mut stack, i, IntPredicate::EQ)?,
397422
Op::Ne => self.cmp_vec(&mut stack, i, IntPredicate::NE)?,
@@ -837,6 +862,85 @@ impl<'ctx, 'a> DualBandLowerer<'ctx, 'a> {
837862
}
838863
}
839864

865+
/// Path D: ArrSetNamed / ArrayIndexAssign helper. Looks up the
866+
/// named array slot, loads the i64-pointer-bit-pattern, inttoptrs
867+
/// to a real LLVM pointer, GEPs to slot `idx + 1` (skipping the
868+
/// length prefix), and stores the value's α lane.
869+
///
870+
/// β is discarded on writes — the value's β was the harmonic
871+
/// shadow of the value at the call site; once written into the
872+
/// array, the slot only holds α (the array's storage is
873+
/// scalar i64). When the value is later READ back via ArrayIndex,
874+
/// it gets a fresh splatted (α, α) pair (matched bands).
875+
///
876+
/// This is a deliberate semantic choice for the MVP: arrays are
877+
/// classical-only storage. β-tracking through arrays would need
878+
/// either parallel β arrays or a wider element type. Leaves
879+
/// the door open for a future "harmonic array" type.
880+
fn emit_array_set_named(
881+
&mut self,
882+
name: &str,
883+
idx_v: VectorValue<'ctx>,
884+
val_v: VectorValue<'ctx>,
885+
op_idx: usize,
886+
) -> Result<(), CodegenError> {
887+
let i64_type = self.ctx.i64_type();
888+
// Look up the slot holding the array's pointer-bit-pattern.
889+
let slot = self.get_or_create_slot(name)?;
890+
// Load the <2 x i64> from the slot, extract α (pointer).
891+
let arr_v_loaded = self
892+
.builder
893+
.build_load(self.v2i64, slot, &format!("{}_arr_load", name))
894+
.map_err(|e| format!("ArrSetNamed slot load at op{}: {}", op_idx, e))?;
895+
let arr_vv = match arr_v_loaded {
896+
BasicValueEnum::VectorValue(vv) => vv,
897+
_ => return Err(format!("ArrSetNamed slot not vector at op{}", op_idx)),
898+
};
899+
let arr_alpha = self
900+
.builder
901+
.build_extract_element(arr_vv, i64_type.const_int(0, false), "asn_aptr")
902+
.map_err(|e| format!("ArrSetNamed extract α at op{}: {}", op_idx, e))?;
903+
let idx_alpha = self
904+
.builder
905+
.build_extract_element(idx_v, i64_type.const_int(0, false), "asn_aix")
906+
.map_err(|e| format!("ArrSetNamed extract idx α at op{}: {}", op_idx, e))?;
907+
let val_alpha = self
908+
.builder
909+
.build_extract_element(val_v, i64_type.const_int(0, false), "asn_aval")
910+
.map_err(|e| format!("ArrSetNamed extract val α at op{}: {}", op_idx, e))?;
911+
let arr_iv = match arr_alpha {
912+
BasicValueEnum::IntValue(iv) => iv,
913+
_ => return Err(format!("ArrSetNamed ptr not int at op{}", op_idx)),
914+
};
915+
let idx_iv = match idx_alpha {
916+
BasicValueEnum::IntValue(iv) => iv,
917+
_ => return Err(format!("ArrSetNamed idx not int at op{}", op_idx)),
918+
};
919+
let val_iv = match val_alpha {
920+
BasicValueEnum::IntValue(iv) => iv,
921+
_ => return Err(format!("ArrSetNamed val not int at op{}", op_idx)),
922+
};
923+
let ptr_ty = self.ctx.ptr_type(inkwell::AddressSpace::default());
924+
let ptr = self
925+
.builder
926+
.build_int_to_ptr(arr_iv, ptr_ty, "asn_ptr")
927+
.map_err(|e| format!("ArrSetNamed inttoptr at op{}: {}", op_idx, e))?;
928+
let one = i64_type.const_int(1, false);
929+
let slot_idx = self
930+
.builder
931+
.build_int_add(idx_iv, one, "asn_slot")
932+
.map_err(|e| format!("ArrSetNamed slot calc at op{}: {}", op_idx, e))?;
933+
let elem_gep = unsafe {
934+
self.builder
935+
.build_in_bounds_gep(i64_type, ptr, &[slot_idx], "asn_gep")
936+
.map_err(|e| format!("ArrSetNamed gep at op{}: {}", op_idx, e))?
937+
};
938+
self.builder
939+
.build_store(elem_gep, val_iv)
940+
.map_err(|e| format!("ArrSetNamed store at op{}: {}", op_idx, e))?;
941+
Ok(())
942+
}
943+
840944
/// Path A.4: ArrayIndex — extract α (pointer) and the user-given
841945
/// scalar index, GEP to slot `idx + 1` (skipping the length
842946
/// prefix), load the element. Returns the element as a scalar i64.

omnimcode-codegen/tests/jit_arrays.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,64 @@ fn jit_array_sum_in_loop() {
8989
assert_eq!(f.call(&[0]).expect("call"), 55); // 1+2+...+10
9090
}
9191

92+
#[test]
93+
fn jit_array_write_with_arr_set() {
94+
// Path D: arr_set in a loop. Build an array of zeros, then fill
95+
// with squares. Verify a known slot.
96+
let source = r#"
97+
fn build_squares(unused) {
98+
h arr = [0, 0, 0, 0, 0];
99+
h k = 0;
100+
while k < 5 {
101+
arr_set(arr, k, k * k);
102+
k = k + 1;
103+
}
104+
return arr_get(arr, 3);
105+
}
106+
"#;
107+
let mut parser = Parser::new(source);
108+
let statements = parser.parse().expect("parse");
109+
let module = omnimcode_core::compiler::compile_program(&statements).expect("compile");
110+
let ctx = Context::create();
111+
let jit = JitContext::new(&ctx).expect("jit");
112+
let jitted = jit.jit_module(&module).expect("jit_module");
113+
let f = jitted.get("build_squares").expect("build_squares JIT'd");
114+
assert_eq!(f.call(&[0]).expect("call"), 9); // 3*3
115+
}
116+
117+
#[test]
118+
fn jit_array_write_then_sum() {
119+
// Build, write into, read back — sum the squares of 0..9.
120+
let source = r#"
121+
fn sum_of_squares(n) {
122+
h arr = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
123+
h k = 0;
124+
while k < n {
125+
arr_set(arr, k, k * k);
126+
k = k + 1;
127+
}
128+
h sum = 0;
129+
h j = 0;
130+
while j < n {
131+
sum = sum + arr_get(arr, j);
132+
j = j + 1;
133+
}
134+
return sum;
135+
}
136+
"#;
137+
let mut parser = Parser::new(source);
138+
let statements = parser.parse().expect("parse");
139+
let module = omnimcode_core::compiler::compile_program(&statements).expect("compile");
140+
let ctx = Context::create();
141+
let jit = JitContext::new(&ctx).expect("jit");
142+
let jitted = jit.jit_module(&module).expect("jit_module");
143+
let f = jitted.get("sum_of_squares").expect("sum_of_squares JIT'd");
144+
// 0² + 1² + 2² + … + 9² = 285
145+
assert_eq!(f.call(&[10]).expect("call"), 285);
146+
// 0² + 1² + 2² + 3² + 4² = 30
147+
assert_eq!(f.call(&[5]).expect("call"), 30);
148+
}
149+
92150
#[test]
93151
fn jit_array_via_dispatch_hook() {
94152
// End-to-end through Interpreter dispatch (matches CLI's

omnimcode-codegen/tests/jit_floats.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,52 @@ fn float_arithmetic_via_to_float() {
7777
assert_eq!(f.call(&[100]).expect("call"), 10_000);
7878
}
7979

80+
#[test]
81+
fn cross_fn_float_passing() {
82+
// Path D verification: floats can flow across fn boundaries
83+
// because they're encoded as i64-bit-pattern on the operand
84+
// stack. Caller's Op::Call passes scalar i64; callee's
85+
// bind_params_into_locals stores i64 into the slot; LoadVar
86+
// returns i64; AddFloat bitcasts at use. No special boundary
87+
// logic needed — the i64 encoding is the universal calling
88+
// convention.
89+
let source = r#"
90+
fn double_it(x) {
91+
return x + x;
92+
}
93+
fn caller(n) {
94+
h xf = to_float(n);
95+
h doubled = double_it(xf);
96+
return to_int(doubled);
97+
}
98+
"#;
99+
let mut parser = Parser::new(source);
100+
let statements = parser.parse().expect("parse");
101+
let module = omnimcode_core::compiler::compile_program(&statements).expect("compile");
102+
let ctx = Context::create();
103+
let jit = JitContext::new(&ctx).expect("jit");
104+
let jitted = jit.jit_module(&module).expect("jit_module");
105+
let f = jitted.get("caller").expect("caller JIT'd");
106+
// n=21: xf = 21.0, double_it(21.0) = 42.0, to_int = 42
107+
// BUT: double_it sees the i64 bit pattern of 21.0, adds it to
108+
// itself as integer (Op::Add not AddFloat), producing garbage.
109+
// This test documents the LIMITATION: cross-fn float passing
110+
// works only when both sides agree on the type AT THE BYTECODE
111+
// LEVEL. double_it has no type info on x, so it emits Op::Add
112+
// (int add of bit patterns) → wrong answer.
113+
//
114+
// The correct cross-fn-float pattern requires explicit float-
115+
// typed ops on both sides. With the OMC compiler emitting plain
116+
// Op::Add for untyped inputs, the only way to guarantee correct
117+
// cross-fn float math today is to pass via ints and convert at
118+
// each fn boundary. Documented for honesty.
119+
let r = f.call(&[21]).expect("call");
120+
// The exact value depends on the bit-pattern arithmetic; what
121+
// matters for this test is that the call doesn't crash and
122+
// produces some deterministic answer.
123+
let _ = r;
124+
}
125+
80126
#[test]
81127
fn float_loop_accumulator() {
82128
// Float Add/Sub/Mul in a loop. Computes

0 commit comments

Comments
 (0)