Skip to content

Commit 5aa84bd

Browse files
committed
IxVM kernel: native unconstrained_big_uint_div_mod Aiur op + small wins
* Aiur op `unconstrained_big_uint_div_mod(a: KLimbs, b: KLimbs) -> (KLimbs, KLimbs)`: runtime computes (q, r) via `num_bigint::BigUint::div_rem` and inserts both result lists into `memory[10]` content-addressed with multiplicity 0; no constraints emitted. Wired through bytecode/execute/constraints/trace + FFI decoder + Source/Typed/Simple/Concrete/Bytecode stages + Meta syntax + Compiler/{Check, Concretize, Simple, Match, Lower, Layout}. Lean reference evaluators (Interpret/SourceEval/BytecodeEval) are stubbed with TODOs; src/aiur/execute.rs is the authoritative impl. * Primitive.lean: `klimbs_div_mod` now reads (q, r) from the op then verifies `klimbs_normalize(klimbs_add(klimbs_mul(q, b), r)) == klimbs_normalize(a)` and `klimbs_lt(r, b) == 1` when `b != 0`. Drops recursive `klimbs_div_mod_go`: its 2^24-step memo growth on (2^32, 256) was the klimbs OOM driver. * Core.lean: route list_lookup through list_drop so sublist ptrs dedup via Aiur's content-addressed store. * Infer.lean: collapse App-spine inference into one helper that peels the head's Forall telescope arg-by-arg using a single expr_inst_many per step on the ORIGINAL dom (with a growing innermost-first subs list) instead of N consecutive expr_inst1 rewalks of a residual cod. * Tests/Ix/IxVM.lean: re-pin 37 FFT costs. lake exe check Nat.add_comm: 55_504_714 -> 54_350_107 FFT (-2.1%). lake exe check '_private.Init.Data.String.Decode.0.ByteArray.utf8DecodeChar?.helper₃': previously OOM (~1.2 GB single allocation inside klimbs_div_mod_go); now passes at 37_793_684_978 FFT.
1 parent ad7e383 commit 5aa84bd

24 files changed

Lines changed: 416 additions & 71 deletions

Ix/Aiur/Compiler/Check.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,13 @@ def inferTerm (t : Term) : CheckM Typed.Term := match t with
787787
let a' ← checkNoEscape a .field
788788
let b' ← checkNoEscape b .field
789789
pure (Typed.Term.u8RangeCheck (.tuple #[.u8, .u8]) false a' b')
790+
| .unconstrainedBigUintDivMod a b => do
791+
-- Both inputs must be the same type (expected `List<U64>` at runtime,
792+
-- but the type-checker is generic: any container will type-check, and
793+
-- the runtime BigUint::div_rem will fault on a malformed shape).
794+
let a' ← inferNoEscape a
795+
let b' ← checkNoEscape b a'.typ
796+
pure (Typed.Term.unconstrainedBigUintDivMod (.tuple #[a'.typ, a'.typ]) false a' b')
790797
| .toField a => do
791798
let a' ← checkNoEscape a .u8
792799
pure (Typed.Term.toField .field false a')
@@ -965,6 +972,8 @@ def zonkTypedTerm (t : Typed.Term) : CheckM Typed.Term := match t with
965972
pure (.u32LessThan (← zonkTyp τ) e (← zonkTypedTerm a) (← zonkTypedTerm b))
966973
| .u8RangeCheck τ e a b => do
967974
pure (.u8RangeCheck (← zonkTyp τ) e (← zonkTypedTerm a) (← zonkTypedTerm b))
975+
| .unconstrainedBigUintDivMod τ e a b => do
976+
pure (.unconstrainedBigUintDivMod (← zonkTyp τ) e (← zonkTypedTerm a) (← zonkTypedTerm b))
968977
| .toField τ e a => do pure (.toField (← zonkTyp τ) e (← zonkTypedTerm a))
969978
| .u8FromFieldUnsafe τ e a => do
970979
pure (.u8FromFieldUnsafe (← zonkTyp τ) e (← zonkTypedTerm a))

Ix/Aiur/Compiler/Concretize.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ def termToConcrete
363363
| .u8RangeCheck τ e a b => do
364364
pure (.u8RangeCheck (← typToConcrete mono τ) e
365365
(← termToConcrete mono a) (← termToConcrete mono b))
366+
| .unconstrainedBigUintDivMod τ e a b => do
367+
pure (.unconstrainedBigUintDivMod (← typToConcrete mono τ) e
368+
(← termToConcrete mono a) (← termToConcrete mono b))
366369
-- `toField` / `u8FromFieldUnsafe` are erased coercions: `u8` and `field`
367370
-- share a representation, so we drop the wrapper and keep the inner term.
368371
| .toField _ _ a | .u8FromFieldUnsafe _ _ a => termToConcrete mono a
@@ -570,6 +573,8 @@ def rewriteTypedTerm (decls : Typed.Decls)
570573
(rewriteTypedTerm decls subst mono a) (rewriteTypedTerm decls subst mono b)
571574
| .u8RangeCheck τ e a b => .u8RangeCheck (rewriteTyp subst mono τ) e
572575
(rewriteTypedTerm decls subst mono a) (rewriteTypedTerm decls subst mono b)
576+
| .unconstrainedBigUintDivMod τ e a b => .unconstrainedBigUintDivMod (rewriteTyp subst mono τ) e
577+
(rewriteTypedTerm decls subst mono a) (rewriteTypedTerm decls subst mono b)
573578
| .toField τ e a => .toField (rewriteTyp subst mono τ) e
574579
(rewriteTypedTerm decls subst mono a)
575580
| .u8FromFieldUnsafe τ e a => .u8FromFieldUnsafe (rewriteTyp subst mono τ) e
@@ -642,6 +647,7 @@ def collectInTypedTerm (seen : Std.HashSet (Global × Array Typ)) :
642647
| .add τ _ a b | .sub τ _ a b | .mul τ _ a b
643648
| .u8Xor τ _ a b | .u8Add τ _ a b | .u8Mul τ _ a b | .u8Sub τ _ a b
644649
| .u8ChainRotr7 τ _ a b | .u8ChainRotr4 τ _ a b | .u8RangeCheck τ _ a b
650+
| .unconstrainedBigUintDivMod τ _ a b
645651
| .u8And τ _ a b | .u8Or τ _ a b
646652
| .u8LessThan τ _ a b | .u32LessThan τ _ a b =>
647653
collectInTypedTerm (collectInTypedTerm (collectInTyp seen τ) a) b
@@ -712,6 +718,7 @@ def collectCalls (decls : Typed.Decls)
712718
| .add _ _ a b | .sub _ _ a b | .mul _ _ a b
713719
| .u8Xor _ _ a b | .u8Add _ _ a b | .u8Mul _ _ a b | .u8Sub _ _ a b
714720
| .u8ChainRotr7 _ _ a b | .u8ChainRotr4 _ _ a b | .u8RangeCheck _ _ a b
721+
| .unconstrainedBigUintDivMod _ _ a b
715722
| .u8And _ _ a b | .u8Or _ _ a b
716723
| .u8LessThan _ _ a b | .u32LessThan _ _ a b =>
717724
collectCalls decls (collectCalls decls seen a) b
@@ -830,6 +837,8 @@ def substInTypedTerm (subst : Global → Option Typ) : Typed.Term → Typed.Term
830837
(substInTypedTerm subst a) (substInTypedTerm subst b)
831838
| .u8RangeCheck τ e a b => .u8RangeCheck (Typ.instantiate subst τ) e
832839
(substInTypedTerm subst a) (substInTypedTerm subst b)
840+
| .unconstrainedBigUintDivMod τ e a b => .unconstrainedBigUintDivMod (Typ.instantiate subst τ) e
841+
(substInTypedTerm subst a) (substInTypedTerm subst b)
833842
| .toField τ e a => .toField (Typ.instantiate subst τ) e (substInTypedTerm subst a)
834843
| .u8FromFieldUnsafe τ e a =>
835844
.u8FromFieldUnsafe (Typ.instantiate subst τ) e (substInTypedTerm subst a)

Ix/Aiur/Compiler/Layout.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ def opLayout : Bytecode.Op → LayoutM Unit
203203
| .u32LessThan .. => do pushDegree 1; bumpAuxiliaries 12; bumpLookups 6
204204
-- Pure range-check lookup: no output columns/degrees, just one lookup.
205205
| .u8RangeCheck .. => bumpLookups
206+
-- Unconstrained hint: two fresh auxiliary witness columns (q_ptr, r_ptr).
207+
-- No constraint relation, no lookup. Mirrors `IORead` (aux only) — the Rust
208+
-- `constraints.rs` pushes exactly 2 auxiliaries and emits no relation.
209+
| .unconstrainedBigUintDivMod .. => do pushDegrees #[1, 1]; bumpAuxiliaries 2
206210
| .debug .. => pure ()
207211

208212
/-- Termination helper for blockLayout's Block/Ctrl traversal. -/

Ix/Aiur/Compiler/Lower.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ def toIndex
326326
let j ← expectIdx layoutMap bindings j
327327
modify fun stt => { stt with ops := stt.ops.push (.u8RangeCheck i j) }
328328
pure #[i, j]
329+
| .unconstrainedBigUintDivMod _ _ a b => do
330+
-- Unconstrained hint: runtime computes q,r via BigUint::div_rem and writes
331+
-- two fresh pointers (q_ptr, r_ptr) into the witness columns. No constraint
332+
-- relation is emitted; caller verifies `q*b + r == a` and `r < b` separately.
333+
let a ← expectIdx layoutMap bindings a
334+
let b ← expectIdx layoutMap bindings b
335+
pushOp (.unconstrainedBigUintDivMod a b) 2
329336
| .debug _ _ label term ret => do
330337
let term ← match term with
331338
| none => pure none

Ix/Aiur/Compiler/Match.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ def typedToSimple : Term → Simple.Term
390390
| .u8LessThan τ e a b => .u8LessThan τ e (typedToSimple a) (typedToSimple b)
391391
| .u32LessThan τ e a b => .u32LessThan τ e (typedToSimple a) (typedToSimple b)
392392
| .u8RangeCheck τ e a b => .u8RangeCheck τ e (typedToSimple a) (typedToSimple b)
393+
| .unconstrainedBigUintDivMod τ e a b => .unconstrainedBigUintDivMod τ e (typedToSimple a) (typedToSimple b)
393394
| .toField τ e a => .toField τ e (typedToSimple a)
394395
| .u8FromFieldUnsafe τ e a => .u8FromFieldUnsafe τ e (typedToSimple a)
395396
| .debug τ e l t r =>

Ix/Aiur/Compiler/Simple.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ def simplifyTypedTerm (decls : Source.Decls) : Term → Except CheckError Term
132132
let a' ← simplifyTypedTerm decls a
133133
let b' ← simplifyTypedTerm decls b
134134
pure (.u8RangeCheck τ e a' b')
135+
| .unconstrainedBigUintDivMod τ e a b => do
136+
let a' ← simplifyTypedTerm decls a
137+
let b' ← simplifyTypedTerm decls b
138+
pure (.unconstrainedBigUintDivMod τ e a' b')
135139
| .toField τ e a => do
136140
let a' ← simplifyTypedTerm decls a
137141
pure (.toField τ e a')

Ix/Aiur/Interpret.lean

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,17 @@ partial def interp (decls : Decls) (bindings : Bindings) : Term → InterpM Valu
406406
if a.val < 256 && b.val < 256 then return .tuple #[.field a, .field b]
407407
else throwErr "u8RangeCheck: value out of range [0, 256)"
408408
| _, _ => throwErr "u8RangeCheck: expected field values"
409+
| .unconstrainedBigUintDivMod t1 t2 => do
410+
-- TODO(unconstrainedBigUintDivMod): walk both List<U64> pointer chains via the
411+
-- store to extract Vec<u8> bytes (LE), interpret as BigUints, compute
412+
-- div_rem natively, build two fresh ListNode chains for q and r, and
413+
-- return `.tuple #[.pointer w q_ptr, .pointer w r_ptr]`. The Rust
414+
-- runtime (execute.rs) already does this; the Lean debug interpreter
415+
-- doesn't yet have BigUint or klimbs helpers, so we surface an explicit
416+
-- error rather than silently returning a wrong value.
417+
let _ ← interp decls bindings t1
418+
let _ ← interp decls bindings t2
419+
throwErr "unconstrainedBigUintDivMod: not implemented in debug interpreter"
409420
-- `toField` / `u8FromFieldUnsafe` are erased coercions: value unchanged.
410421
| .toField t | .u8FromFieldUnsafe t => interp decls bindings t
411422
| .u8Lit n => return .field (G.ofNat n)

Ix/Aiur/Meta.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ syntax "u8_or" "(" aiur_trm ", " aiur_trm ")" : ai
188188
syntax "u8_less_than" "(" aiur_trm ", " aiur_trm ")" : aiur_trm
189189
syntax "u32_less_than" "(" aiur_trm ", " aiur_trm ")" : aiur_trm
190190
syntax "u8_range_check" "(" aiur_trm ", " aiur_trm ")" : aiur_trm
191+
syntax "unconstrained_big_uint_div_mod" "(" aiur_trm ", " aiur_trm ")" : aiur_trm
191192
syntax "to_field" "(" aiur_trm ")" : aiur_trm
192193
syntax "u8_from_field_unsafe" "(" aiur_trm ")" : aiur_trm
193194
syntax:max num "u8" : aiur_trm
@@ -323,6 +324,8 @@ partial def elabTrm : ElabStxCat `aiur_trm
323324
mkAppM ``Source.Term.u32LessThan #[← elabTrm i, ← elabTrm j]
324325
| `(aiur_trm| u8_range_check($i:aiur_trm, $j:aiur_trm)) => do
325326
mkAppM ``Source.Term.u8RangeCheck #[← elabTrm i, ← elabTrm j]
327+
| `(aiur_trm| unconstrained_big_uint_div_mod($a:aiur_trm, $b:aiur_trm)) => do
328+
mkAppM ``Source.Term.unconstrainedBigUintDivMod #[← elabTrm a, ← elabTrm b]
326329
| `(aiur_trm| to_field($a:aiur_trm)) => do
327330
mkAppM ``Source.Term.toField #[← elabTrm a]
328331
| `(aiur_trm| u8_from_field_unsafe($a:aiur_trm)) => do
@@ -547,6 +550,10 @@ where
547550
let i ← replaceToken old new i
548551
let j ← replaceToken old new j
549552
`(aiur_trm| u8_range_check($i, $j))
553+
| `(aiur_trm| unconstrained_big_uint_div_mod($a:aiur_trm, $b:aiur_trm)) => do
554+
let a ← replaceToken old new a
555+
let b ← replaceToken old new b
556+
`(aiur_trm| unconstrained_big_uint_div_mod($a, $b))
550557
| `(aiur_trm| to_field($a:aiur_trm)) => do
551558
let a ← replaceToken old new a
552559
`(aiur_trm| to_field($a))

Ix/Aiur/Semantics/BytecodeEval.lean

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ inductive BytecodeError
4040
| callOutputSizeMismatch
4141
| unreachableAfterLayout
4242
| u8RangeCheckFailed
43+
| unconstrainedBigUintDivModUnsupported
4344
deriving Repr, Inhabited
4445

4546
/-- Width-bucketed memory, matching Rust's `QueryRecord.memory_queries`.
@@ -293,6 +294,15 @@ def evalOp (t : Bytecode.Toplevel) (fuel : Nat) (op : Op) (st : EvalState) :
293294
-- outside `[0, 256)` (exactly what the byte-chip lookup enforces).
294295
let x ← readIdx st a; let y ← readIdx st b
295296
if x.val < 256 && y.val < 256 then .ok st else .error .u8RangeCheckFailed
297+
| .unconstrainedBigUintDivMod a b => do
298+
-- TODO(unconstrainedBigUintDivMod): walk the two pointer chains in `st.memory`
299+
-- (List<U64> = ListNode of [U8;8]), extract LE bytes, compute BigUint
300+
-- div_rem, build two fresh ListNode chains in `st.memory`, push their
301+
-- pointer ValIdxs. The Rust runtime already does this end-to-end; the
302+
-- reference evaluator doesn't yet have BigUint helpers, so we surface
303+
-- an explicit error rather than silently producing wrong values.
304+
let _ ← readIdx st a; let _ ← readIdx st b
305+
.error .unconstrainedBigUintDivModUnsupported
296306
| .debug _ _ => .ok st
297307
termination_by (fuel, sizeOf op, 0)
298308
decreasing_by all_goals first | decreasing_tactic | omega

Ix/Aiur/Semantics/SourceEval.lean

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,19 @@ def interp (decls : Decls) (fuel : Nat) (bindings : Bindings)
444444
.ok (.tuple #[.field a, .field b], st2)
445445
else .error (.typeMismatch "u8RangeCheck")
446446
| _, _ => .error (.typeMismatch "u8RangeCheck")
447+
| .unconstrainedBigUintDivMod t1 t2 =>
448+
-- TODO(unconstrainedBigUintDivMod): walk both List<U64> pointer chains via the
449+
-- store to extract Vec<u8> bytes (LE), compute BigUint div_rem, build two
450+
-- fresh ListNode chains, and return `.tuple #[.pointer w q_ptr, .pointer w r_ptr]`.
451+
-- The Rust runtime already does this; the reference semantics doesn't yet
452+
-- have klimbs/BigUint helpers. Surfacing typeMismatch keeps the source
453+
-- evaluator total without committing to a half-baked semantics.
454+
match interp decls fuel bindings t1 st with
455+
| .error e => .error e
456+
| .ok (_, st1) =>
457+
match interp decls fuel bindings t2 st1 with
458+
| .error e => .error e
459+
| .ok (_, _) => .error (.typeMismatch "unconstrainedBigUintDivMod")
447460
-- `toField` / `u8FromFieldUnsafe` are erased coercions: value unchanged.
448461
| .toField t | .u8FromFieldUnsafe t => interp decls fuel bindings t st
449462
| .u8Lit n => .ok (.field (G.ofNat n), st)

0 commit comments

Comments
 (0)