Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
27e3d34
Add DAG-based kernel typechecker
johnchandlerburnham Feb 6, 2026
5eec6ca
Merge branch 'main' of github.com:argumentcomputer/ix into jcb/rust-k…
johnchandlerburnham Feb 15, 2026
13da42f
WIP kernel
johnchandlerburnham Feb 20, 2026
ff92399
reenable printing type of erroring constants
johnchandlerburnham Feb 20, 2026
14380d8
move error printing to end to unhide if types are long
johnchandlerburnham Feb 20, 2026
c77d309
correctness improvements and ST caching
johnchandlerburnham Feb 20, 2026
904c3fb
Rewrite Lean kernel from NbE to environment-based substitution
johnchandlerburnham Mar 2, 2026
406a7a3
Add EquivManager, inferOnly mode, and isDefEq optimizations
johnchandlerburnham Mar 2, 2026
573abad
Make positivity checking monadic with whnf and nested inductive support
johnchandlerburnham Mar 2, 2026
0055ecc
Iterativize binder chains and fix recursor validation for Ixon blocks
johnchandlerburnham Mar 3, 2026
cb5fe1a
Replace HashMap with TreeMap and iterativize Expr traversals
johnchandlerburnham Mar 3, 2026
c3f16c6
Unify recursion depth tracking, move caches before fuel guards, and i…
johnchandlerburnham Mar 5, 2026
3f08273
Add let-bound bvar zeta-reduction, fix proof irrelevance and K-reduction
johnchandlerburnham Mar 5, 2026
92069bc
Fix nat reduction to whnf args, use content-based def-eq cache keys
johnchandlerburnham Mar 6, 2026
7abd736
Add primitive validation, recursor rule type checking, and merge whn…
johnchandlerburnham Mar 6, 2026
51728e7
Add Kernel2 NbE type checker and improve Kernel1 level/whnf handling
johnchandlerburnham Mar 9, 2026
6b76c65
Rename Kernel2 → Kernel, delete old environment-based kernel
johnchandlerburnham Mar 9, 2026
9015ef2
Add symbolic nat reduction, inference cache, and bidirectional checking
johnchandlerburnham Mar 10, 2026
53b85c8
Fix O(n) nat literal peeling in extractSuccPred and parse numeric n…
johnchandlerburnham Mar 10, 2026
8c5dcc5
Major optimizations to both Lean and Rust NbE type checkers:
johnchandlerburnham Mar 10, 2026
de46c34
Improve kernel correctness and add comprehensive parity tests
johnchandlerburnham Mar 11, 2026
e90d2b3
Introduce MetaId as unified constant identifier across Lean and Rus…
johnchandlerburnham Mar 13, 2026
987fe49
Eliminate StateT from TypecheckM: all mutable state via ST.Ref
johnchandlerburnham Mar 13, 2026
12818d3
debugging instrumentation
johnchandlerburnham Mar 13, 2026
0f6743a
Kernel performance optimizations, correctness fixes, and SemType form…
johnchandlerburnham Mar 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,19 @@
# Nix
result*
.direnv/

# LaTeX build artifacts
whitepaper/*.aux
whitepaper/*.log
whitepaper/*.out
whitepaper/*.toc
whitepaper/*.bbl
whitepaper/*.blg
whitepaper/*.bcf
whitepaper/*.run.xml
whitepaper/*.fdb_latexmk
whitepaper/*.fls
whitepaper/*.idx
whitepaper/*.ilg
whitepaper/*.ind
whitepaper/*.synctex.gz
12 changes: 11 additions & 1 deletion Ix/Address.lean
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ structure Address where
/-- Compute the Blake3 hash of a `ByteArray`, returning an `Address`. -/
def Address.blake3 (x: ByteArray) : Address := ⟨(Blake3.hash x).val⟩


/-- Convert a nibble (0--15) to its lowercase hexadecimal character. -/
def hexOfNat : Nat -> Option Char
| 0 => .some '0'
Expand Down Expand Up @@ -76,8 +77,17 @@ instance : ToString Address where
instance : Repr Address where
reprPrec a _ := "#" ++ (toString a).toFormat

private def compareBytesLoop (a b : ByteArray) (i : Nat) : Ordering :=
if i >= a.size then .eq
else
let va := a.get! i
let vb := b.get! i
if va < vb then .lt
else if va > vb then .gt
else compareBytesLoop a b (i + 1)

instance : Ord Address where
compare a b := compare a.hash.data.toList b.hash.data.toList
compare a b := compareBytesLoop a.hash b.hash 0

instance : Inhabited Address where
default := Address.blake3 ⟨#[]⟩
Expand Down
122 changes: 122 additions & 0 deletions Ix/Cli/CheckCmd.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import Cli
import Ix.Common
import Ix.Kernel
import Ix.Meta
import Ix.CompileM
import Lean

open System (FilePath)

/-- If the project depends on Mathlib, download the Mathlib cache. -/
private def fetchMathlibCache (cwd : Option FilePath) : IO Unit := do
let root := cwd.getD "."
let manifest := root / "lake-manifest.json"
let contents ← IO.FS.readFile manifest
if contents.containsSubstr "leanprover-community/mathlib4" then
let mathlibBuild := root / ".lake" / "packages" / "mathlib" / ".lake" / "build"
if ← mathlibBuild.pathExists then
println! "Mathlib cache already present, skipping fetch."
return
println! "Detected Mathlib dependency. Fetching Mathlib cache..."
let child ← IO.Process.spawn {
cmd := "lake"
args := #["exe", "cache", "get"]
cwd := cwd
stdout := .inherit
stderr := .inherit
}
let exitCode ← child.wait
if exitCode != 0 then
throw $ IO.userError "lake exe cache get failed"

/-- Build the Lean module at the given file path using Lake. -/
private def buildFile (path : FilePath) : IO Unit := do
let path ← IO.FS.realPath path
let some moduleName := path.fileStem
| throw $ IO.userError s!"cannot determine module name from {path}"
fetchMathlibCache path.parent
let child ← IO.Process.spawn {
cmd := "lake"
args := #["build", moduleName]
cwd := path.parent
stdout := .inherit
stderr := .inherit
}
let exitCode ← child.wait
if exitCode != 0 then
throw $ IO.userError "lake build failed"

/-- Run the Lean kernel checker. -/
private def runLeanCheck (leanEnv : Lean.Environment) : IO UInt32 := do
println! "Compiling to Ixon..."
let compileStart ← IO.monoMsNow
let ixonEnv ← Ix.CompileM.rsCompileEnv leanEnv
let compileElapsed := (← IO.monoMsNow) - compileStart
let numConsts := ixonEnv.consts.size
println! "Compiled {numConsts} constants in {compileElapsed.formatMs}"

println! "Converting Ixon → Kernel..."
let convertStart ← IO.monoMsNow
match Ix.Kernel.Convert.convertEnv .meta ixonEnv with
| .error e =>
println! "Conversion error: {e}"
return 1
| .ok (kenv, prims, quotInit) =>
let convertElapsed := (← IO.monoMsNow) - convertStart
println! "Converted {kenv.size} constants in {convertElapsed.formatMs}"

println! "Typechecking..."
let checkStart ← IO.monoMsNow
match Ix.Kernel.typecheckAll kenv prims quotInit with
| .error e =>
let elapsed := (← IO.monoMsNow) - checkStart
println! "Kernel check failed in {elapsed.formatMs}: {e}"
return 1
| .ok () =>
let elapsed := (← IO.monoMsNow) - checkStart
println! "Checked {kenv.size} constants in {elapsed.formatMs}"
return 0

/-- Run the Rust kernel checker. -/
private def runRustCheck (leanEnv : Lean.Environment) : IO UInt32 := do
let totalConsts := leanEnv.constants.toList.length
println! "Total constants: {totalConsts}"

let start ← IO.monoMsNow
let errors ← Ix.Kernel.rsCheckEnv leanEnv
let elapsed := (← IO.monoMsNow) - start

if errors.isEmpty then
println! "Checked {totalConsts} constants in {elapsed.formatMs}"
return 0
else
println! "Kernel check failed with {errors.size} error(s) in {elapsed.formatMs}:"
for (name, err) in errors[:min 50 errors.size] do
println! " {repr name}: {repr err}"
return 1

def runCheckCmd (p : Cli.Parsed) : IO UInt32 := do
let some path := p.flag? "path"
| p.printError "error: must specify --path"
return 1
let pathStr := path.as! String
let useLean := p.hasFlag "lean"

buildFile pathStr
let leanEnv ← getFileEnv pathStr

if useLean then
println! "Running Lean kernel checker on {pathStr}"
runLeanCheck leanEnv
else
println! "Running Rust kernel checker on {pathStr}"
runRustCheck leanEnv

def checkCmd : Cli.Cmd := `[Cli|
check VIA runCheckCmd;
"Type-check Lean file with kernel"

FLAGS:
path : String; "Path to file to check"
lean; "Use Lean kernel instead of Rust kernel"
]
16 changes: 6 additions & 10 deletions Ix/CompileM.lean
Original file line number Diff line number Diff line change
Expand Up @@ -1604,11 +1604,10 @@ def compileEnv (env : Ix.Environment) (blocks : Ix.CondensedBlocks) (dbg : Bool

-- Build reverse index and names map, storing name string components as blobs
-- Seed with blockNames collected during compilation (binder names, level params, etc.)
let (addrToNameMap, namesMap, nameBlobs) :=
compileEnv.nameToNamed.fold (init := ({}, blockNames, {})) fun (addrMap, namesMap, blobs) name named =>
let addrMap := addrMap.insert named.addr name
let (namesMap, nameBlobs) :=
compileEnv.nameToNamed.fold (init := (blockNames, {})) fun (namesMap, blobs) name _named =>
let (namesMap, blobs) := Ixon.RawEnv.addNameComponentsWithBlobs namesMap blobs name
(addrMap, namesMap, blobs)
(namesMap, blobs)

-- Merge name string blobs into the main blobs map
let allBlobs := nameBlobs.fold (fun m k v => m.insert k v) compileEnv.blobs
Expand All @@ -1619,7 +1618,6 @@ def compileEnv (env : Ix.Environment) (blocks : Ix.CondensedBlocks) (dbg : Bool
blobs := allBlobs
names := namesMap
comms := {}
addrToName := addrToNameMap
}

return .ok (ixonEnv, compileEnv.totalBytes)
Expand Down Expand Up @@ -1890,11 +1888,10 @@ def compileEnvParallel (env : Ix.Environment) (blocks : Ix.CondensedBlocks)

-- Build reverse index and names map, storing name string components as blobs
-- Seed with blockNames collected during compilation (binder names, level params, etc.)
let (addrToNameMap, namesMap, nameBlobs) :=
nameToNamed.fold (init := ({}, blockNames, {})) fun (addrMap, namesMap, nameBlobs) name named =>
let addrMap := addrMap.insert named.addr name
let (namesMap, nameBlobs) :=
nameToNamed.fold (init := (blockNames, {})) fun (namesMap, nameBlobs) name _named =>
let (namesMap, nameBlobs) := Ixon.RawEnv.addNameComponentsWithBlobs namesMap nameBlobs name
(addrMap, namesMap, nameBlobs)
(namesMap, nameBlobs)

-- Merge name string blobs into the main blobs map
let blockBlobCount := blobs.size
Expand All @@ -1912,7 +1909,6 @@ def compileEnvParallel (env : Ix.Environment) (blocks : Ix.CondensedBlocks)
blobs := allBlobs
names := namesMap
comms := {}
addrToName := addrToNameMap
}

return .ok (ixonEnv, totalBytes)
Expand Down
24 changes: 6 additions & 18 deletions Ix/DecompileM.lean
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,6 @@ def lookupNameAddrOrAnon (addr : Address) : DecompileM Ix.Name := do
| some n => pure n
| none => pure Ix.Name.mkAnon

/-- Resolve constant Address → Ix.Name via addrToName. -/
def lookupConstName (addr : Address) : DecompileM Ix.Name := do
match (← getEnv).ixonEnv.addrToName.get? addr with
| some n => pure n
| none => throw (.missingAddress addr)

def lookupBlob (addr : Address) : DecompileM ByteArray := do
match (← getEnv).ixonEnv.blobs.get? addr with
| some blob => pure blob
Expand Down Expand Up @@ -390,18 +384,14 @@ partial def decompileExpr (e : Ixon.Expr) (arenaIdx : UInt64) : DecompileM Ix.Ex
pure (applyMdata (Ix.Expr.mkLit (.strVal s)) mdataLayers)

-- Ref with arena metadata
| .ref nameAddr, .ref refIdx univIndices => do
let name ← match (← getEnv).ixonEnv.names.get? nameAddr with
| some n => pure n
| none => getRef refIdx >>= lookupConstName
| .ref nameAddr, .ref _refIdx univIndices => do
let name ← lookupNameAddr nameAddr
let lvls ← decompileUnivIndices univIndices
pure (applyMdata (Ix.Expr.mkConst name lvls) mdataLayers)

-- Ref without arena metadata
| _, .ref refIdx univIndices => do
let name ← getRef refIdx >>= lookupConstName
let lvls ← decompileUnivIndices univIndices
pure (applyMdata (Ix.Expr.mkConst name lvls) mdataLayers)
| _, .ref _refIdx _univIndices => do
throw (.badConstantFormat "ref without arena metadata")

-- Rec with arena metadata
| .ref nameAddr, .recur recIdx univIndices => do
Expand Down Expand Up @@ -472,10 +462,8 @@ partial def decompileExpr (e : Ixon.Expr) (arenaIdx : UInt64) : DecompileM Ix.Ex
let valExpr ← decompileExpr val child
pure (applyMdata (Ix.Expr.mkProj typeName fieldIdx.toNat valExpr) mdataLayers)

| _, .prj typeRefIdx fieldIdx val => do
let typeName ← getRef typeRefIdx >>= lookupConstName
let valExpr ← decompileExpr val UInt64.MAX
pure (applyMdata (Ix.Expr.mkProj typeName fieldIdx.toNat valExpr) mdataLayers)
| _, .prj _typeRefIdx _fieldIdx _val => do
throw (.badConstantFormat "prj without arena metadata")

| _, .share _ => throw (.badConstantFormat "unexpected Share in decompileExpr")

Expand Down
16 changes: 4 additions & 12 deletions Ix/Ixon.lean
Original file line number Diff line number Diff line change
Expand Up @@ -1380,12 +1380,10 @@ structure Env where
named : Std.HashMap Ix.Name Named := {}
/-- Raw data blobs: Address → bytes -/
blobs : Std.HashMap Address ByteArray := {}
/-- Hash-consed name components: Address → Ix.Name -/
names : Std.HashMap Address Ix.Name := {}
/-- Cryptographic commitments: Address → Comm -/
comms : Std.HashMap Address Comm := {}
/-- Reverse index: constant Address → Ix.Name -/
addrToName : Std.HashMap Address Ix.Name := {}
/-- Hash-consed name components: Address → Ix.Name -/
names : Std.HashMap Address Ix.Name := {}
deriving Inhabited

namespace Env
Expand All @@ -1401,8 +1399,7 @@ def getConst? (env : Env) (addr : Address) : Option Constant :=
/-- Register a name with full Named metadata. -/
def registerName (env : Env) (name : Ix.Name) (named : Named) : Env :=
{ env with
named := env.named.insert name named
addrToName := env.addrToName.insert named.addr name }
named := env.named.insert name named }

/-- Register a name with just an address (empty metadata). -/
def registerNameAddr (env : Env) (name : Ix.Name) (addr : Address) : Env :=
Expand All @@ -1416,10 +1413,6 @@ def getAddr? (env : Env) (name : Ix.Name) : Option Address :=
def getNamed? (env : Env) (name : Ix.Name) : Option Named :=
env.named.get? name

/-- Look up an address's name. -/
def getName? (env : Env) (addr : Address) : Option Ix.Name :=
env.addrToName.get? addr

/-- Store a blob and return its content address. -/
def storeBlob (env : Env) (bytes : ByteArray) : Env × Address :=
let addr := Address.blake3 bytes
Expand Down Expand Up @@ -1742,8 +1735,7 @@ def getEnv : GetM Env := do
| some name =>
let namedEntry : Named := ⟨constAddr, constMeta⟩
env := { env with
named := env.named.insert name namedEntry
addrToName := env.addrToName.insert constAddr name }
named := env.named.insert name namedEntry }
| none =>
throw s!"getEnv: named entry references unknown name address {reprStr (toString nameAddr)}"

Expand Down
67 changes: 67 additions & 0 deletions Ix/Kernel.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Lean
import Ix.Environment
import Ix.Kernel.Types
import Ix.Kernel.Datatypes
import Ix.Kernel.Level
import Ix.Kernel.ExprUtils
import Ix.Kernel.Value
import Ix.Kernel.EquivManager
import Ix.Kernel.TypecheckM
import Ix.Kernel.Helpers
import Ix.Kernel.Quote
import Ix.Kernel.Primitive
import Ix.Kernel.Infer
import Ix.Kernel.Convert

namespace Ix.Kernel

/-- Type-checking errors from the Rust kernel, mirroring `TcError` in Rust. -/
inductive CheckError where
| typeExpected (expr : Ix.Expr) (inferred : Ix.Expr)
| functionExpected (expr : Ix.Expr) (inferred : Ix.Expr)
| typeMismatch (expected : Ix.Expr) (found : Ix.Expr) (expr : Ix.Expr)
| defEqFailure (lhs : Ix.Expr) (rhs : Ix.Expr)
| unknownConst (name : Ix.Name)
| duplicateUniverse (name : Ix.Name)
| freeBoundVariable (idx : UInt64)
| kernelException (msg : String)
deriving Repr

/-- FFI: Run Rust NbE type-checker over all declarations in a Lean environment. -/
@[extern "rs_check_env"]
opaque rsCheckEnvFFI : @& List (Lean.Name × Lean.ConstantInfo) → IO (Array (Ix.Name × CheckError))

/-- Check all declarations in a Lean environment using the Rust kernel.
Returns an array of (name, error) pairs for any declarations that fail. -/
def rsCheckEnv (leanEnv : Lean.Environment) : IO (Array (Ix.Name × CheckError)) :=
rsCheckEnvFFI leanEnv.constants.toList

/-- FFI: Type-check a single constant by dotted name string. -/
@[extern "rs_check_const"]
opaque rsCheckConstFFI : @& List (Lean.Name × Lean.ConstantInfo) → @& String → IO (Option CheckError)

/-- Check a single constant by name using the Rust kernel.
Returns `none` on success, `some err` on failure. -/
def rsCheckConst (leanEnv : Lean.Environment) (name : String) : IO (Option CheckError) :=
rsCheckConstFFI leanEnv.constants.toList name

/-- FFI: Type-check a batch of constants by name.
Converts the environment once, then checks each name.
Returns an array of (name, Option error) pairs. -/
@[extern "rs_check_consts"]
opaque rsCheckConstsFFI : @& List (Lean.Name × Lean.ConstantInfo) → @& Array String → IO (Array (String × Option CheckError))

/-- Check a batch of constants by name using the Rust NbE checker. -/
def rsCheckConsts (leanEnv : Lean.Environment) (names : Array String) : IO (Array (String × Option CheckError)) :=
rsCheckConstsFFI leanEnv.constants.toList names

/-- FFI: Convert env to Kernel types without type-checking.
Returns diagnostic strings: status, kenv_size, prims_found, quot_init, missing prims. -/
@[extern "rs_convert_env"]
opaque rsConvertEnvFFI : @& List (Lean.Name × Lean.ConstantInfo) → IO (Array String)

/-- Convert env to Kernel types using Rust. Returns diagnostic array. -/
def rsConvertEnv (leanEnv : Lean.Environment) : IO (Array String) :=
rsConvertEnvFFI leanEnv.constants.toList

end Ix.Kernel
Loading
Loading