Skip to content

Commit 0fe4f76

Browse files
J4: Op::DivFloat + EqFloat/NeFloat/LtFloat/LeFloat/GtFloat/GeFloat
Closes the float-typed op gap from Path A.2. Previously the OMC bytecode compiler emitted plain Op::Div + Op::Eq/Ne/Lt/etc regardless of operand types, so the JIT (which trusts the typed op) treated float bit-patterns as int and produced garbage on anything beyond Add/Sub/Mul. What's new: - Op::DivFloat in bytecode.rs (compiler emits when both operands are statically typed-float) - Op::EqFloat / NeFloat / LtFloat / LeFloat / GtFloat / GeFloat for the comparison family - Compiler specialization in Expression::Div, Eq, Ne, Lt, Le, Gt, Ge — uses existing infer_type to choose Op::*Float when both sides are float-typed - VM handlers in vm.rs: DivFloat returns Singularity on /0 (matches Op::Div semantics); comparisons inline as direct f64 comparisons - JIT scalar lowerer: DivFloat via bin_float, comparisons via new cmp_op_float helper using FloatPredicate (OEQ/ONE/OLT/etc) - JIT dual-band lowerer: same ops via bin_vec_float + cmp_vec_float, parallel-lane <2 x f64> bitcast - disasm.rs: pretty-print mnemonics for the new ops Test added (5 in jit_floats now, all passing): - float_div_and_compare_in_jit: harmonic_x1000(n) = floor(H_n * 1000) using `1.0 / to_float(k)` in a loop. Previously failed in JIT because Op::Div was integer-coercing; now H_10 = 2929 matches the closed-form value. Workspace: 42 codegen tests pass (was 41), 149 core unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ae3bb63 commit 0fe4f76

7 files changed

Lines changed: 256 additions & 7 deletions

File tree

omnimcode-codegen/src/dual_band.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ impl<'ctx, 'a> DualBandLowerer<'ctx, 'a> {
330330
// through int math (matched-band semantics until an
331331
// explicit phi_shadow re-derives β).
332332
Op::AddFloat => self.bin_vec_float(&mut stack, i, |b, l, r| b.build_float_add(l, r, "fadd"))?,
333+
Op::DivFloat => self.bin_vec_float(&mut stack, i, |b, l, r| b.build_float_div(l, r, "fdiv"))?,
333334
Op::SubFloat => self.bin_vec_float(&mut stack, i, |b, l, r| b.build_float_sub(l, r, "fsub"))?,
334335
Op::MulFloat => self.bin_vec_float(&mut stack, i, |b, l, r| b.build_float_mul(l, r, "fmul"))?,
335336
Op::Sub | Op::SubInt => self.bin_vec(&mut stack, i, |b, l, r| b.build_int_sub(l, r, "sub"))?,
@@ -424,6 +425,13 @@ impl<'ctx, 'a> DualBandLowerer<'ctx, 'a> {
424425
Op::Le => self.cmp_vec(&mut stack, i, IntPredicate::SLE)?,
425426
Op::Gt => self.cmp_vec(&mut stack, i, IntPredicate::SGT)?,
426427
Op::Ge => self.cmp_vec(&mut stack, i, IntPredicate::SGE)?,
428+
// J4: parallel-lane float comparisons.
429+
Op::EqFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::OEQ)?,
430+
Op::NeFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::ONE)?,
431+
Op::LtFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::OLT)?,
432+
Op::LeFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::OLE)?,
433+
Op::GtFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::OGT)?,
434+
Op::GeFloat => self.cmp_vec_float(&mut stack, i, inkwell::FloatPredicate::OGE)?,
427435

428436
Op::And => self.logical_vec(&mut stack, i, true)?,
429437
Op::Or => self.logical_vec(&mut stack, i, false)?,
@@ -1223,6 +1231,41 @@ impl<'ctx, 'a> DualBandLowerer<'ctx, 'a> {
12231231
Ok(())
12241232
}
12251233

1234+
/// J4: parallel-lane float comparison. Symmetric to bin_vec_float
1235+
/// — bitcast <2 x i64> stack operands to <2 x f64>, compare with
1236+
/// FloatPredicate, zext result back to <2 x i64>.
1237+
fn cmp_vec_float(
1238+
&self,
1239+
stack: &mut Vec<VectorValue<'ctx>>,
1240+
op_idx: usize,
1241+
pred: inkwell::FloatPredicate,
1242+
) -> Result<(), CodegenError> {
1243+
let f64_type = self.ctx.f64_type();
1244+
let v2f64 = f64_type.vec_type(2);
1245+
let rhs = self.pop(stack, op_idx, "fcmp rhs")?;
1246+
let lhs = self.pop(stack, op_idx, "fcmp lhs")?;
1247+
let lhs_f = self
1248+
.builder
1249+
.build_bit_cast(lhs, v2f64, "fcmp_lf")
1250+
.map_err(|e| format!("hbit fcmp lhs cast at op{}: {}", op_idx, e))?
1251+
.into_vector_value();
1252+
let rhs_f = self
1253+
.builder
1254+
.build_bit_cast(rhs, v2f64, "fcmp_rf")
1255+
.map_err(|e| format!("hbit fcmp rhs cast at op{}: {}", op_idx, e))?
1256+
.into_vector_value();
1257+
let cmp_i1 = self
1258+
.builder
1259+
.build_float_compare(pred, lhs_f, rhs_f, "fcmp")
1260+
.map_err(|e| format!("hbit fcmp at op{}: {}", op_idx, e))?;
1261+
let cmp_i64 = self
1262+
.builder
1263+
.build_int_z_extend(cmp_i1, self.v2i64, "fcmp_i64")
1264+
.map_err(|e| format!("hbit fcmp extend at op{}: {}", op_idx, e))?;
1265+
stack.push(cmp_i64);
1266+
Ok(())
1267+
}
1268+
12261269
fn logical_vec(
12271270
&self,
12281271
stack: &mut Vec<VectorValue<'ctx>>,

omnimcode-codegen/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ impl<'ctx, 'a> FunctionLowerer<'ctx, 'a> {
632632
Op::AddFloat => self.bin_float(&mut stack, i, |b, l, r| b.build_float_add(l, r, "fadd"))?,
633633
Op::SubFloat => self.bin_float(&mut stack, i, |b, l, r| b.build_float_sub(l, r, "fsub"))?,
634634
Op::MulFloat => self.bin_float(&mut stack, i, |b, l, r| b.build_float_mul(l, r, "fmul"))?,
635+
Op::DivFloat => self.bin_float(&mut stack, i, |b, l, r| b.build_float_div(l, r, "fdiv"))?,
635636
Op::Neg => {
636637
let v = pop(&mut stack, i, "Neg")?;
637638
let zero = i64_type.const_int(0, false);
@@ -662,6 +663,17 @@ impl<'ctx, 'a> FunctionLowerer<'ctx, 'a> {
662663
Op::Le => self.cmp_op(&mut stack, i, IntPredicate::SLE)?,
663664
Op::Gt => self.cmp_op(&mut stack, i, IntPredicate::SGT)?,
664665
Op::Ge => self.cmp_op(&mut stack, i, IntPredicate::SGE)?,
666+
// J4: float-typed comparisons. Bitcast i64 stack
667+
// operands to f64, compare with FloatPredicate, zext
668+
// result back to i64 for stack storage. OEQ/ONE/etc
669+
// are "ordered" predicates — return false on NaN
670+
// operands, matching standard float comparison semantics.
671+
Op::EqFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::OEQ)?,
672+
Op::NeFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::ONE)?,
673+
Op::LtFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::OLT)?,
674+
Op::LeFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::OLE)?,
675+
Op::GtFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::OGT)?,
676+
Op::GeFloat => self.cmp_op_float(&mut stack, i, inkwell::FloatPredicate::OGE)?,
665677

666678
Op::And => {
667679
// Non-short-circuit: pop both, treat zero as false,
@@ -991,6 +1003,42 @@ impl<'ctx, 'a> FunctionLowerer<'ctx, 'a> {
9911003
stack.push(i64v);
9921004
Ok(())
9931005
}
1006+
1007+
/// J4: float comparison. Bitcast i64 stack operands back to f64,
1008+
/// compare with FloatPredicate (ordered: O*), zext result to i64.
1009+
/// Symmetric to bin_float — operands live as bitcast-i64 on the
1010+
/// stack; we cast at the boundary.
1011+
fn cmp_op_float(
1012+
&self,
1013+
stack: &mut Vec<IntValue<'ctx>>,
1014+
op_idx: usize,
1015+
pred: inkwell::FloatPredicate,
1016+
) -> Result<(), CodegenError> {
1017+
let rhs_i = pop(stack, op_idx, "fcmp rhs")?;
1018+
let lhs_i = pop(stack, op_idx, "fcmp lhs")?;
1019+
let f64_type = self.ctx.f64_type();
1020+
let i64_type = self.ctx.i64_type();
1021+
let lhs_f = self
1022+
.builder
1023+
.build_bit_cast(lhs_i, f64_type, "fcmp_lf")
1024+
.map_err(|e| format!("fcmp lhs cast at op{}: {}", op_idx, e))?
1025+
.into_float_value();
1026+
let rhs_f = self
1027+
.builder
1028+
.build_bit_cast(rhs_i, f64_type, "fcmp_rf")
1029+
.map_err(|e| format!("fcmp rhs cast at op{}: {}", op_idx, e))?
1030+
.into_float_value();
1031+
let i1 = self
1032+
.builder
1033+
.build_float_compare(pred, lhs_f, rhs_f, "fcmp")
1034+
.map_err(|e| format!("fcmp at op{}: {}", op_idx, e))?;
1035+
let i64v = self
1036+
.builder
1037+
.build_int_z_extend(i1, i64_type, "fcmp_i64")
1038+
.map_err(|e| format!("fcmp ext at op{}: {}", op_idx, e))?;
1039+
stack.push(i64v);
1040+
Ok(())
1041+
}
9941042
}
9951043

9961044
fn pop<'ctx>(

omnimcode-codegen/tests/jit_floats.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,55 @@ fn cross_fn_float_passing() {
123123
let _ = r;
124124
}
125125

126+
#[test]
127+
fn float_div_and_compare_in_jit() {
128+
// J4 verification: typed-float Div + comparisons compile cleanly
129+
// and produce correct answers in the JIT path. Computes the
130+
// partial harmonic series H_n that float_loop_accumulator's old
131+
// version couldn't because Op::Div was integer-coercing the float
132+
// bit-pattern.
133+
//
134+
// The compiler emits DivFloat when both operands are statically
135+
// typed-float (the `1.0 / to_float(k)` shape).
136+
let source = r#"
137+
fn harmonic_x1000(n) {
138+
h sum = 0.0;
139+
h k = 1;
140+
while k <= n {
141+
sum = sum + 1.0 / to_float(k);
142+
k = k + 1;
143+
}
144+
return to_int(sum * 1000.0);
145+
}
146+
fn float_lt(a, b) {
147+
h af = to_float(a);
148+
h bf = to_float(b);
149+
if af < bf {
150+
return 1;
151+
}
152+
return 0;
153+
}
154+
"#;
155+
let mut parser = Parser::new(source);
156+
let statements = parser.parse().expect("parse");
157+
let module = omnimcode_core::compiler::compile_program(&statements).expect("compile");
158+
let ctx = Context::create();
159+
let jit = JitContext::new(&ctx).expect("jit");
160+
let jitted = jit.jit_module(&module).expect("jit_module");
161+
162+
let h = jitted.get("harmonic_x1000").expect("harmonic_x1000 JIT'd");
163+
assert_eq!(h.call(&[1]).expect("call"), 1000);
164+
assert_eq!(h.call(&[2]).expect("call"), 1500);
165+
assert_eq!(h.call(&[3]).expect("call"), 1833);
166+
let h10 = h.call(&[10]).expect("call");
167+
assert!(h10 >= 2928 && h10 <= 2930, "H_10*1000 ~= 2929; got {}", h10);
168+
169+
let lt = jitted.get("float_lt").expect("float_lt JIT'd");
170+
assert_eq!(lt.call(&[1, 2]).expect("call"), 1);
171+
assert_eq!(lt.call(&[5, 5]).expect("call"), 0);
172+
assert_eq!(lt.call(&[10, 3]).expect("call"), 0);
173+
}
174+
126175
#[test]
127176
fn float_loop_accumulator() {
128177
// Float Add/Sub/Mul in a loop. Computes

omnimcode-core/src/bytecode.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,29 @@ pub enum Op {
6161
AddFloat,
6262
SubFloat,
6363
MulFloat,
64+
/// J4: float division. Plain Op::Div coerces both operands to int
65+
/// in the tree-walk and bytecode VM, giving wrong answers for
66+
/// float operands. The JIT path also reads operands as int. This
67+
/// op is emitted by the compiler when both sides are statically
68+
/// typed-float; tree-walk and VM treat it as float div, JIT
69+
/// bitcasts and emits build_float_div.
70+
DivFloat,
6471

6572
Eq,
6673
Ne,
6774
Lt,
6875
Le,
6976
Gt,
7077
Ge,
78+
/// J4: float comparisons. Plain Eq/Ne/Lt/Le/Gt/Ge call values_equal
79+
/// or to_int comparison; both wrong for float bit-patterns. Emitted
80+
/// when both operands are statically typed-float.
81+
EqFloat,
82+
NeFloat,
83+
LtFloat,
84+
LeFloat,
85+
GtFloat,
86+
GeFloat,
7187

7288
And,
7389
Or,

omnimcode-core/src/compiler.rs

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,44 +320,79 @@ impl Compiler {
320320
};
321321
}
322322
Expression::Div(l, r) => {
323+
let lt = self.infer_type(l);
324+
let rt = self.infer_type(r);
323325
self.compile_expr(l)?;
324326
self.compile_expr(r)?;
325-
self.emit(Op::Div);
327+
match (lt, rt) {
328+
(Some("float"), Some("float")) => self.emit(Op::DivFloat),
329+
_ => self.emit(Op::Div),
330+
};
326331
}
327332
Expression::Mod(l, r) => {
328333
self.compile_expr(l)?;
329334
self.compile_expr(r)?;
330335
self.emit(Op::Mod);
331336
}
332337
Expression::Eq(l, r) => {
338+
let lt = self.infer_type(l);
339+
let rt = self.infer_type(r);
333340
self.compile_expr(l)?;
334341
self.compile_expr(r)?;
335-
self.emit(Op::Eq);
342+
match (lt, rt) {
343+
(Some("float"), Some("float")) => self.emit(Op::EqFloat),
344+
_ => self.emit(Op::Eq),
345+
};
336346
}
337347
Expression::Ne(l, r) => {
348+
let lt = self.infer_type(l);
349+
let rt = self.infer_type(r);
338350
self.compile_expr(l)?;
339351
self.compile_expr(r)?;
340-
self.emit(Op::Ne);
352+
match (lt, rt) {
353+
(Some("float"), Some("float")) => self.emit(Op::NeFloat),
354+
_ => self.emit(Op::Ne),
355+
};
341356
}
342357
Expression::Lt(l, r) => {
358+
let lt = self.infer_type(l);
359+
let rt = self.infer_type(r);
343360
self.compile_expr(l)?;
344361
self.compile_expr(r)?;
345-
self.emit(Op::Lt);
362+
match (lt, rt) {
363+
(Some("float"), Some("float")) => self.emit(Op::LtFloat),
364+
_ => self.emit(Op::Lt),
365+
};
346366
}
347367
Expression::Le(l, r) => {
368+
let lt = self.infer_type(l);
369+
let rt = self.infer_type(r);
348370
self.compile_expr(l)?;
349371
self.compile_expr(r)?;
350-
self.emit(Op::Le);
372+
match (lt, rt) {
373+
(Some("float"), Some("float")) => self.emit(Op::LeFloat),
374+
_ => self.emit(Op::Le),
375+
};
351376
}
352377
Expression::Gt(l, r) => {
378+
let lt = self.infer_type(l);
379+
let rt = self.infer_type(r);
353380
self.compile_expr(l)?;
354381
self.compile_expr(r)?;
355-
self.emit(Op::Gt);
382+
match (lt, rt) {
383+
(Some("float"), Some("float")) => self.emit(Op::GtFloat),
384+
_ => self.emit(Op::Gt),
385+
};
356386
}
357387
Expression::Ge(l, r) => {
388+
let lt = self.infer_type(l);
389+
let rt = self.infer_type(r);
358390
self.compile_expr(l)?;
359391
self.compile_expr(r)?;
360-
self.emit(Op::Ge);
392+
match (lt, rt) {
393+
(Some("float"), Some("float")) => self.emit(Op::GeFloat),
394+
_ => self.emit(Op::Ge),
395+
};
361396
}
362397
Expression::And(l, r) => {
363398
// Short-circuit: eval l; if false, push false and skip r.

omnimcode-core/src/disasm.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,20 @@ fn op_mnemonic(op: &Op, ip: usize, constants: &[Const]) -> String {
3535
Op::AddFloat => "ADD_FLOAT".to_string(),
3636
Op::SubFloat => "SUB_FLOAT".to_string(),
3737
Op::MulFloat => "MUL_FLOAT".to_string(),
38+
Op::DivFloat => "DIV_FLOAT".to_string(),
3839

3940
Op::Eq => "EQ".to_string(),
4041
Op::Ne => "NE".to_string(),
4142
Op::Lt => "LT".to_string(),
4243
Op::Le => "LE".to_string(),
4344
Op::Gt => "GT".to_string(),
4445
Op::Ge => "GE".to_string(),
46+
Op::EqFloat => "EQ_FLOAT".to_string(),
47+
Op::NeFloat => "NE_FLOAT".to_string(),
48+
Op::LtFloat => "LT_FLOAT".to_string(),
49+
Op::LeFloat => "LE_FLOAT".to_string(),
50+
Op::GtFloat => "GT_FLOAT".to_string(),
51+
Op::GeFloat => "GE_FLOAT".to_string(),
4552

4653
Op::And => "AND".to_string(),
4754
Op::Or => "OR".to_string(),

omnimcode-core/src/vm.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ impl Vm {
205205
let l = stack.pop().ok_or("stack underflow")?.to_float();
206206
stack.push(Value::HFloat(l * r));
207207
}
208+
Op::DivFloat => {
209+
let r = stack.pop().ok_or("stack underflow")?.to_float();
210+
let l = stack.pop().ok_or("stack underflow")?.to_float();
211+
if r == 0.0 {
212+
// Match Op::Div's singularity semantics: divide
213+
// by zero produces a Singularity value carrying
214+
// the numerator. Tree-walk uses arith_div for
215+
// this; here we inline since DivFloat is purely
216+
// float-typed.
217+
stack.push(Value::Singularity {
218+
numerator: l as i64,
219+
denominator: 0,
220+
context: "divide by zero".to_string(),
221+
});
222+
} else {
223+
stack.push(Value::HFloat(l / r));
224+
}
225+
}
208226
Op::Neg => {
209227
let v = stack.pop().ok_or("stack underflow")?;
210228
if v.is_float() {
@@ -219,6 +237,39 @@ impl Vm {
219237
let cmp = cmp_op(&l, &r, op);
220238
stack.push(Value::Bool(cmp));
221239
}
240+
// J4: float-typed comparisons. Skip the runtime
241+
// is_float() probe in cmp_op — operands are statically
242+
// typed-float by construction.
243+
Op::EqFloat => {
244+
let r = stack.pop().ok_or("stack underflow")?.to_float();
245+
let l = stack.pop().ok_or("stack underflow")?.to_float();
246+
stack.push(Value::Bool(l == r));
247+
}
248+
Op::NeFloat => {
249+
let r = stack.pop().ok_or("stack underflow")?.to_float();
250+
let l = stack.pop().ok_or("stack underflow")?.to_float();
251+
stack.push(Value::Bool(l != r));
252+
}
253+
Op::LtFloat => {
254+
let r = stack.pop().ok_or("stack underflow")?.to_float();
255+
let l = stack.pop().ok_or("stack underflow")?.to_float();
256+
stack.push(Value::Bool(l < r));
257+
}
258+
Op::LeFloat => {
259+
let r = stack.pop().ok_or("stack underflow")?.to_float();
260+
let l = stack.pop().ok_or("stack underflow")?.to_float();
261+
stack.push(Value::Bool(l <= r));
262+
}
263+
Op::GtFloat => {
264+
let r = stack.pop().ok_or("stack underflow")?.to_float();
265+
let l = stack.pop().ok_or("stack underflow")?.to_float();
266+
stack.push(Value::Bool(l > r));
267+
}
268+
Op::GeFloat => {
269+
let r = stack.pop().ok_or("stack underflow")?.to_float();
270+
let l = stack.pop().ok_or("stack underflow")?.to_float();
271+
stack.push(Value::Bool(l >= r));
272+
}
222273
Op::And => {
223274
let r = stack.pop().ok_or("stack underflow")?;
224275
let l = stack.pop().ok_or("stack underflow")?;

0 commit comments

Comments
 (0)