on master help sign in
Implement Cranelift-based compiler with SQLite persistence

Add workspace restructure with three crates: knot (frontend), knot-runtime (Rust staticlib with extern C, bundled SQLite via rusqlite), knot-compiler (Cranelift codegen + linker + CLI). Compiler generates native ARM64 binaries with full relational query support, schema-driven SQLite persistence, closures with captured environments, and nested loop do-block compilation. All 230 parser tests pass. Includes hello.knot and todo.knot examples demonstrating source relations, do-comprehensions, set mutations, and filtering.

commitb4723093b7c24500fa98d565e8c5d8e3ab445773 authoriko <mail@iko.soy> dateMon 9 Mar 2026, 17:56 (7 months ago) parentc1ca808
18 files changed+3234−3
Cargo.lock+462 Cargo.toml+15−3 crates/knot-compiler/Cargo.toml+18 crates/knot-compiler/src/codegen.rs+1488 crates/knot-compiler/src/linker.rs+36 crates/knot-compiler/src/main.rs+131 crates/knot-compiler/src/types.rs+153 crates/knot-runtime/Cargo.toml+10 crates/knot-runtime/src/lib.rs+882 crates/knot/Cargo.toml+4 src/ast.rs → crates/knot/src/ast.rs src/diagnostic.rs → crates/knot/src/diagnostic.rs src/lexer.rs → crates/knot/src/lexer.rs src/lib.rs → crates/knot/src/lib.rs src/parser.rs → crates/knot/src/parser.rs tests/parser.rs → crates/knot/tests/parser.rs examples/hello.knot+9 examples/todo.knot+26
Cargo.lock+462
@@ -2,6 +2,468 @@
22# It is not intended for manual editing.
33version = 4
44
5+[[package]]
6+name = "allocator-api2"
7+version = "0.2.21"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
10+
11+[[package]]
12+name = "anyhow"
13+version = "1.0.102"
14+source = "registry+https://github.com/rust-lang/crates.io-index"
15+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
16+
17+[[package]]
18+name = "arbitrary"
19+version = "1.4.2"
20+source = "registry+https://github.com/rust-lang/crates.io-index"
21+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
22+
23+[[package]]
24+name = "bitflags"
25+version = "2.11.0"
26+source = "registry+https://github.com/rust-lang/crates.io-index"
27+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
28+
29+[[package]]
30+name = "bumpalo"
31+version = "3.20.2"
32+source = "registry+https://github.com/rust-lang/crates.io-index"
33+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
34+dependencies = [
35+ "allocator-api2",
36+]
37+
38+[[package]]
39+name = "cc"
40+version = "1.2.56"
41+source = "registry+https://github.com/rust-lang/crates.io-index"
42+checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
43+dependencies = [
44+ "find-msvc-tools",
45+ "shlex",
46+]
47+
48+[[package]]
49+name = "cfg-if"
50+version = "1.0.4"
51+source = "registry+https://github.com/rust-lang/crates.io-index"
52+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
53+
54+[[package]]
55+name = "cranelift-bforest"
56+version = "0.116.1"
57+source = "registry+https://github.com/rust-lang/crates.io-index"
58+checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4"
59+dependencies = [
60+ "cranelift-entity",
61+]
62+
63+[[package]]
64+name = "cranelift-bitset"
65+version = "0.116.1"
66+source = "registry+https://github.com/rust-lang/crates.io-index"
67+checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34"
68+
69+[[package]]
70+name = "cranelift-codegen"
71+version = "0.116.1"
72+source = "registry+https://github.com/rust-lang/crates.io-index"
73+checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e"
74+dependencies = [
75+ "bumpalo",
76+ "cranelift-bforest",
77+ "cranelift-bitset",
78+ "cranelift-codegen-meta",
79+ "cranelift-codegen-shared",
80+ "cranelift-control",
81+ "cranelift-entity",
82+ "cranelift-isle",
83+ "gimli",
84+ "hashbrown 0.14.5",
85+ "log",
86+ "regalloc2",
87+ "rustc-hash",
88+ "serde",
89+ "smallvec",
90+ "target-lexicon",
91+]
92+
93+[[package]]
94+name = "cranelift-codegen-meta"
95+version = "0.116.1"
96+source = "registry+https://github.com/rust-lang/crates.io-index"
97+checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8"
98+dependencies = [
99+ "cranelift-codegen-shared",
100+]
101+
102+[[package]]
103+name = "cranelift-codegen-shared"
104+version = "0.116.1"
105+source = "registry+https://github.com/rust-lang/crates.io-index"
106+checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb"
107+
108+[[package]]
109+name = "cranelift-control"
110+version = "0.116.1"
111+source = "registry+https://github.com/rust-lang/crates.io-index"
112+checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef"
113+dependencies = [
114+ "arbitrary",
115+]
116+
117+[[package]]
118+name = "cranelift-entity"
119+version = "0.116.1"
120+source = "registry+https://github.com/rust-lang/crates.io-index"
121+checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323"
122+dependencies = [
123+ "cranelift-bitset",
124+]
125+
126+[[package]]
127+name = "cranelift-frontend"
128+version = "0.116.1"
129+source = "registry+https://github.com/rust-lang/crates.io-index"
130+checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57"
131+dependencies = [
132+ "cranelift-codegen",
133+ "log",
134+ "smallvec",
135+ "target-lexicon",
136+]
137+
138+[[package]]
139+name = "cranelift-isle"
140+version = "0.116.1"
141+source = "registry+https://github.com/rust-lang/crates.io-index"
142+checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d"
143+
144+[[package]]
145+name = "cranelift-module"
146+version = "0.116.1"
147+source = "registry+https://github.com/rust-lang/crates.io-index"
148+checksum = "4d55612bebcf16ff7306c8a6f5bdb6d45662b8aa1ee058ecce8807ad87db719b"
149+dependencies = [
150+ "anyhow",
151+ "cranelift-codegen",
152+ "cranelift-control",
153+]
154+
155+[[package]]
156+name = "cranelift-native"
157+version = "0.116.1"
158+source = "registry+https://github.com/rust-lang/crates.io-index"
159+checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7"
160+dependencies = [
161+ "cranelift-codegen",
162+ "libc",
163+ "target-lexicon",
164+]
165+
166+[[package]]
167+name = "cranelift-object"
168+version = "0.116.1"
169+source = "registry+https://github.com/rust-lang/crates.io-index"
170+checksum = "aad5a6d3e379493c3f8b35dc61c93d0bf5f27003bbe20614e0200b0ec372ef52"
171+dependencies = [
172+ "anyhow",
173+ "cranelift-codegen",
174+ "cranelift-control",
175+ "cranelift-module",
176+ "log",
177+ "object",
178+ "target-lexicon",
179+]
180+
181+[[package]]
182+name = "crc32fast"
183+version = "1.5.0"
184+source = "registry+https://github.com/rust-lang/crates.io-index"
185+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
186+dependencies = [
187+ "cfg-if",
188+]
189+
190+[[package]]
191+name = "equivalent"
192+version = "1.0.2"
193+source = "registry+https://github.com/rust-lang/crates.io-index"
194+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
195+
196+[[package]]
197+name = "fallible-iterator"
198+version = "0.3.0"
199+source = "registry+https://github.com/rust-lang/crates.io-index"
200+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
201+
202+[[package]]
203+name = "fallible-streaming-iterator"
204+version = "0.1.9"
205+source = "registry+https://github.com/rust-lang/crates.io-index"
206+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
207+
208+[[package]]
209+name = "find-msvc-tools"
210+version = "0.1.9"
211+source = "registry+https://github.com/rust-lang/crates.io-index"
212+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
213+
214+[[package]]
215+name = "foldhash"
216+version = "0.1.5"
217+source = "registry+https://github.com/rust-lang/crates.io-index"
218+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
219+
220+[[package]]
221+name = "gimli"
222+version = "0.31.1"
223+source = "registry+https://github.com/rust-lang/crates.io-index"
224+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
225+dependencies = [
226+ "fallible-iterator",
227+ "indexmap",
228+ "stable_deref_trait",
229+]
230+
231+[[package]]
232+name = "hashbrown"
233+version = "0.14.5"
234+source = "registry+https://github.com/rust-lang/crates.io-index"
235+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
236+
237+[[package]]
238+name = "hashbrown"
239+version = "0.15.5"
240+source = "registry+https://github.com/rust-lang/crates.io-index"
241+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
242+dependencies = [
243+ "foldhash",
244+]
245+
246+[[package]]
247+name = "hashbrown"
248+version = "0.16.1"
249+source = "registry+https://github.com/rust-lang/crates.io-index"
250+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
251+
252+[[package]]
253+name = "hashlink"
254+version = "0.10.0"
255+source = "registry+https://github.com/rust-lang/crates.io-index"
256+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
257+dependencies = [
258+ "hashbrown 0.15.5",
259+]
260+
261+[[package]]
262+name = "indexmap"
263+version = "2.13.0"
264+source = "registry+https://github.com/rust-lang/crates.io-index"
265+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
266+dependencies = [
267+ "equivalent",
268+ "hashbrown 0.16.1",
269+]
270+
5271[[package]]
6272name = "knot"
7273version = "0.1.0"
274+
275+[[package]]
276+name = "knot-compiler"
277+version = "0.1.0"
278+dependencies = [
279+ "cranelift-codegen",
280+ "cranelift-frontend",
281+ "cranelift-module",
282+ "cranelift-native",
283+ "cranelift-object",
284+ "knot",
285+ "knot-runtime",
286+ "target-lexicon",
287+]
288+
289+[[package]]
290+name = "knot-runtime"
291+version = "0.1.0"
292+dependencies = [
293+ "rusqlite",
294+]
295+
296+[[package]]
297+name = "libc"
298+version = "0.2.183"
299+source = "registry+https://github.com/rust-lang/crates.io-index"
300+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
301+
302+[[package]]
303+name = "libsqlite3-sys"
304+version = "0.32.0"
305+source = "registry+https://github.com/rust-lang/crates.io-index"
306+checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
307+dependencies = [
308+ "cc",
309+ "pkg-config",
310+ "vcpkg",
311+]
312+
313+[[package]]
314+name = "log"
315+version = "0.4.29"
316+source = "registry+https://github.com/rust-lang/crates.io-index"
317+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
318+
319+[[package]]
320+name = "memchr"
321+version = "2.8.0"
322+source = "registry+https://github.com/rust-lang/crates.io-index"
323+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
324+
325+[[package]]
326+name = "object"
327+version = "0.36.7"
328+source = "registry+https://github.com/rust-lang/crates.io-index"
329+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
330+dependencies = [
331+ "crc32fast",
332+ "hashbrown 0.15.5",
333+ "indexmap",
334+ "memchr",
335+]
336+
337+[[package]]
338+name = "pkg-config"
339+version = "0.3.32"
340+source = "registry+https://github.com/rust-lang/crates.io-index"
341+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
342+
343+[[package]]
344+name = "proc-macro2"
345+version = "1.0.106"
346+source = "registry+https://github.com/rust-lang/crates.io-index"
347+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
348+dependencies = [
349+ "unicode-ident",
350+]
351+
352+[[package]]
353+name = "quote"
354+version = "1.0.45"
355+source = "registry+https://github.com/rust-lang/crates.io-index"
356+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
357+dependencies = [
358+ "proc-macro2",
359+]
360+
361+[[package]]
362+name = "regalloc2"
363+version = "0.11.2"
364+source = "registry+https://github.com/rust-lang/crates.io-index"
365+checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a"
366+dependencies = [
367+ "allocator-api2",
368+ "bumpalo",
369+ "hashbrown 0.15.5",
370+ "log",
371+ "rustc-hash",
372+ "smallvec",
373+]
374+
375+[[package]]
376+name = "rusqlite"
377+version = "0.34.0"
378+source = "registry+https://github.com/rust-lang/crates.io-index"
379+checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
380+dependencies = [
381+ "bitflags",
382+ "fallible-iterator",
383+ "fallible-streaming-iterator",
384+ "hashlink",
385+ "libsqlite3-sys",
386+ "smallvec",
387+]
388+
389+[[package]]
390+name = "rustc-hash"
391+version = "2.1.1"
392+source = "registry+https://github.com/rust-lang/crates.io-index"
393+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
394+
395+[[package]]
396+name = "serde"
397+version = "1.0.228"
398+source = "registry+https://github.com/rust-lang/crates.io-index"
399+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
400+dependencies = [
401+ "serde_core",
402+]
403+
404+[[package]]
405+name = "serde_core"
406+version = "1.0.228"
407+source = "registry+https://github.com/rust-lang/crates.io-index"
408+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
409+dependencies = [
410+ "serde_derive",
411+]
412+
413+[[package]]
414+name = "serde_derive"
415+version = "1.0.228"
416+source = "registry+https://github.com/rust-lang/crates.io-index"
417+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
418+dependencies = [
419+ "proc-macro2",
420+ "quote",
421+ "syn",
422+]
423+
424+[[package]]
425+name = "shlex"
426+version = "1.3.0"
427+source = "registry+https://github.com/rust-lang/crates.io-index"
428+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
429+
430+[[package]]
431+name = "smallvec"
432+version = "1.15.1"
433+source = "registry+https://github.com/rust-lang/crates.io-index"
434+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
435+
436+[[package]]
437+name = "stable_deref_trait"
438+version = "1.2.1"
439+source = "registry+https://github.com/rust-lang/crates.io-index"
440+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
441+
442+[[package]]
443+name = "syn"
444+version = "2.0.117"
445+source = "registry+https://github.com/rust-lang/crates.io-index"
446+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
447+dependencies = [
448+ "proc-macro2",
449+ "quote",
450+ "unicode-ident",
451+]
452+
453+[[package]]
454+name = "target-lexicon"
455+version = "0.13.5"
456+source = "registry+https://github.com/rust-lang/crates.io-index"
457+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
458+
459+[[package]]
460+name = "unicode-ident"
461+version = "1.0.24"
462+source = "registry+https://github.com/rust-lang/crates.io-index"
463+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
464+
465+[[package]]
466+name = "vcpkg"
467+version = "0.2.15"
468+source = "registry+https://github.com/rust-lang/crates.io-index"
469+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
Cargo.toml+15−3
@@ -1,6 +1,18 @@
1−[package]
2−name = "knot"
1+[workspace]
2+members = ["crates/*"]
3+resolver = "3"
4+
5+[workspace.package]
36version = "0.1.0"
47edition = "2024"
58
6−[dependencies]
9+[workspace.dependencies]
10+knot = { path = "crates/knot" }
11+knot-runtime = { path = "crates/knot-runtime" }
12+cranelift-codegen = "0.116"
13+cranelift-frontend = "0.116"
14+cranelift-module = "0.116"
15+cranelift-object = "0.116"
16+cranelift-native = "0.116"
17+target-lexicon = "0.13"
18+rusqlite = { version = "0.34", features = ["bundled"] }
crates/knot-compiler/Cargo.tomlnew file+18
@@ -0,0 +1,18 @@
1+[package]
2+name = "knot-compiler"
3+version.workspace = true
4+edition.workspace = true
5+
6+[[bin]]
7+name = "knotc"
8+path = "src/main.rs"
9+
10+[dependencies]
11+knot = { workspace = true }
12+knot-runtime = { workspace = true }
13+cranelift-codegen = { workspace = true }
14+cranelift-frontend = { workspace = true }
15+cranelift-module = { workspace = true }
16+cranelift-object = { workspace = true }
17+cranelift-native = { workspace = true }
18+target-lexicon = { workspace = true }
crates/knot-compiler/src/codegen.rsnew file+1488
@@ -0,0 +1,1488 @@
1+//! Cranelift-based code generator for the Knot language.
2+//!
3+//! Compiles a Knot AST into a native object file. All Knot values are
4+//! represented at the machine level as pointers to heap-allocated tagged
5+//! values (managed by the runtime). The generated code calls into runtime
6+//! functions for value construction, operations, and SQLite persistence.
7+
8+use crate::types::TypeEnv;
9+use cranelift_codegen::ir::condcodes::IntCC;
10+use cranelift_codegen::ir::types;
11+use cranelift_codegen::ir::{AbiParam, InstBuilder, Value};
12+use cranelift_codegen::settings::{self, Configurable};
13+use cranelift_codegen::Context;
14+use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
15+use cranelift_module::{DataDescription, DataId, FuncId, Linkage, Module};
16+use cranelift_object::{ObjectBuilder, ObjectModule};
17+use knot::ast;
18+use std::collections::HashMap;
19+
20+// ── Codegen state ─────────────────────────────────────────────────
21+
22+pub struct Codegen {
23+ module: ObjectModule,
24+ ctx: Context,
25+ builder_ctx: FunctionBuilderContext,
26+ ptr_type: types::Type,
27+
28+ // Interned string constants
29+ strings: HashMap<String, DataId>,
30+ string_counter: usize,
31+
32+ // Runtime function declarations (imported)
33+ runtime_fns: HashMap<String, FuncId>,
34+
35+ // User function declarations: name -> (func_id, param_count)
36+ user_fns: HashMap<String, (FuncId, usize)>,
37+
38+ // Source relation schemas: name -> schema descriptor
39+ source_schemas: HashMap<String, String>,
40+
41+ // Constructor info: ctor_name -> [(field_name, field_type_str)]
42+ constructors: HashMap<String, Vec<(String, String)>>,
43+
44+ // Counter for generating unique lambda names
45+ lambda_counter: usize,
46+
47+ // Pending lambda definitions: (func_id, params, body, free_vars)
48+ pending_lambdas: Vec<PendingLambda>,
49+}
50+
51+struct PendingLambda {
52+ func_id: FuncId,
53+ params: Vec<String>,
54+ body: ast::Expr,
55+ free_vars: Vec<String>,
56+}
57+
58+// ── Variable environment ──────────────────────────────────────────
59+
60+#[derive(Clone)]
61+struct Env {
62+ bindings: HashMap<String, Value>,
63+}
64+
65+impl Env {
66+ fn new() -> Self {
67+ Self {
68+ bindings: HashMap::new(),
69+ }
70+ }
71+
72+ fn get(&self, name: &str) -> Value {
73+ *self.bindings.get(name).unwrap_or_else(|| {
74+ panic!("codegen: undefined variable '{}'", name);
75+ })
76+ }
77+
78+ fn set(&mut self, name: &str, val: Value) {
79+ self.bindings.insert(name.to_string(), val);
80+ }
81+}
82+
83+// ── Loop tracking for do-block compilation ────────────────────────
84+
85+struct LoopInfo {
86+ header: cranelift_codegen::ir::Block,
87+ continue_blk: cranelift_codegen::ir::Block,
88+ exit: cranelift_codegen::ir::Block,
89+ index_var: Value,
90+ where_skips: Vec<cranelift_codegen::ir::Block>,
91+}
92+
93+// ── Public API ────────────────────────────────────────────────────
94+
95+pub fn compile(module: &ast::Module, type_env: &TypeEnv) -> Vec<u8> {
96+ let mut cg = Codegen::new();
97+ cg.source_schemas = type_env.source_schemas.clone();
98+ for (name, fields) in &type_env.constructors {
99+ let field_strs: Vec<(String, String)> = fields
100+ .iter()
101+ .map(|(n, _)| (n.clone(), "unknown".into()))
102+ .collect();
103+ cg.constructors.insert(name.clone(), field_strs);
104+ }
105+ cg.declare_runtime_fns();
106+ cg.collect_declarations(module);
107+ cg.define_functions(module, type_env);
108+ cg.generate_main(module);
109+ cg.finish()
110+}
111+
112+// ── Constructor ───────────────────────────────────────────────────
113+
114+impl Codegen {
115+ fn new() -> Self {
116+ let mut flag_builder = settings::builder();
117+ flag_builder.set("is_pic", "true").unwrap();
118+ let isa_builder =
119+ cranelift_native::builder().expect("failed to detect host CPU");
120+ let isa = isa_builder
121+ .finish(settings::Flags::new(flag_builder))
122+ .expect("failed to build ISA");
123+ let ptr_type = isa.pointer_type();
124+
125+ let builder = ObjectBuilder::new(
126+ isa,
127+ "knot_program",
128+ cranelift_module::default_libcall_names(),
129+ )
130+ .expect("failed to create ObjectBuilder");
131+ let module = ObjectModule::new(builder);
132+
133+ Self {
134+ ctx: module.make_context(),
135+ module,
136+ builder_ctx: FunctionBuilderContext::new(),
137+ ptr_type,
138+ strings: HashMap::new(),
139+ string_counter: 0,
140+ runtime_fns: HashMap::new(),
141+ user_fns: HashMap::new(),
142+ source_schemas: HashMap::new(),
143+ constructors: HashMap::new(),
144+ lambda_counter: 0,
145+ pending_lambdas: Vec::new(),
146+ }
147+ }
148+
149+ // ── Runtime function declarations ─────────────────────────────
150+
151+ fn declare_runtime_fns(&mut self) {
152+ let p = self.ptr_type;
153+
154+ // Value constructors
155+ self.declare_rt("knot_value_int", &[types::I64], &[p]);
156+ self.declare_rt("knot_value_float", &[types::F64], &[p]);
157+ self.declare_rt("knot_value_text", &[p, p], &[p]);
158+ self.declare_rt("knot_value_bool", &[types::I32], &[p]);
159+ self.declare_rt("knot_value_unit", &[], &[p]);
160+ self.declare_rt("knot_value_function", &[p, p], &[p]);
161+ self.declare_rt("knot_value_constructor", &[p, p, p], &[p]);
162+
163+ // Value accessors
164+ self.declare_rt("knot_value_get_int", &[p], &[types::I64]);
165+ self.declare_rt("knot_value_get_float", &[p], &[types::F64]);
166+ self.declare_rt("knot_value_get_bool", &[p], &[types::I32]);
167+
168+ // Record operations
169+ self.declare_rt("knot_record_empty", &[p], &[p]);
170+ self.declare_rt("knot_record_set_field", &[p, p, p, p], &[]);
171+ self.declare_rt("knot_record_field", &[p, p, p], &[p]);
172+ self.declare_rt("knot_record_update", &[p], &[p]);
173+
174+ // Relation operations
175+ self.declare_rt("knot_relation_empty", &[], &[p]);
176+ self.declare_rt("knot_relation_singleton", &[p], &[p]);
177+ self.declare_rt("knot_relation_push", &[p, p], &[]);
178+ self.declare_rt("knot_relation_len", &[p], &[p]);
179+ self.declare_rt("knot_relation_get", &[p, p], &[p]);
180+ self.declare_rt("knot_relation_union", &[p, p], &[p]);
181+
182+ // Binary operations
183+ self.declare_rt("knot_value_add", &[p, p], &[p]);
184+ self.declare_rt("knot_value_sub", &[p, p], &[p]);
185+ self.declare_rt("knot_value_mul", &[p, p], &[p]);
186+ self.declare_rt("knot_value_div", &[p, p], &[p]);
187+ self.declare_rt("knot_value_eq", &[p, p], &[p]);
188+ self.declare_rt("knot_value_neq", &[p, p], &[p]);
189+ self.declare_rt("knot_value_lt", &[p, p], &[p]);
190+ self.declare_rt("knot_value_gt", &[p, p], &[p]);
191+ self.declare_rt("knot_value_le", &[p, p], &[p]);
192+ self.declare_rt("knot_value_ge", &[p, p], &[p]);
193+ self.declare_rt("knot_value_and", &[p, p], &[p]);
194+ self.declare_rt("knot_value_or", &[p, p], &[p]);
195+ self.declare_rt("knot_value_concat", &[p, p], &[p]);
196+
197+ // Unary operations
198+ self.declare_rt("knot_value_negate", &[p], &[p]);
199+ self.declare_rt("knot_value_not", &[p], &[p]);
200+
201+ // Function calls
202+ self.declare_rt("knot_value_call", &[p, p, p], &[p]);
203+
204+ // Printing
205+ self.declare_rt("knot_print", &[p], &[p]);
206+ self.declare_rt("knot_println", &[p], &[p]);
207+
208+ // Database
209+ self.declare_rt("knot_db_open", &[p, p], &[p]);
210+ self.declare_rt("knot_db_close", &[p], &[]);
211+ self.declare_rt("knot_db_exec", &[p, p, p], &[]);
212+ self.declare_rt("knot_source_init", &[p, p, p, p, p], &[]);
213+ self.declare_rt("knot_source_read", &[p, p, p, p, p], &[p]);
214+ self.declare_rt("knot_source_write", &[p, p, p, p, p, p], &[]);
215+
216+ // Transactions
217+ self.declare_rt("knot_atomic_begin", &[p], &[]);
218+ self.declare_rt("knot_atomic_commit", &[p], &[]);
219+
220+ // Constructor matching
221+ self.declare_rt("knot_constructor_matches", &[p, p, p], &[types::I32]);
222+ self.declare_rt("knot_constructor_payload", &[p], &[p]);
223+ }
224+
225+ fn declare_rt(&mut self, name: &str, params: &[types::Type], returns: &[types::Type]) {
226+ let mut sig = self.module.make_signature();
227+ for p in params {
228+ sig.params.push(AbiParam::new(*p));
229+ }
230+ for r in returns {
231+ sig.returns.push(AbiParam::new(*r));
232+ }
233+ let id = self
234+ .module
235+ .declare_function(name, Linkage::Import, &sig)
236+ .unwrap();
237+ self.runtime_fns.insert(name.to_string(), id);
238+ }
239+
240+ // ── Declaration collection ────────────────────────────────────
241+
242+ fn collect_declarations(&mut self, module: &ast::Module) {
243+ for decl in &module.decls {
244+ match &decl.node {
245+ ast::DeclKind::Fun { name, params, .. } => {
246+ let n_params = params.len();
247+ let mut sig = self.module.make_signature();
248+ sig.params.push(AbiParam::new(self.ptr_type)); // db
249+ for _ in 0..n_params {
250+ sig.params.push(AbiParam::new(self.ptr_type));
251+ }
252+ sig.returns.push(AbiParam::new(self.ptr_type));
253+ let func_name = format!("knot_user_{}", name);
254+ let func_id = self
255+ .module
256+ .declare_function(&func_name, Linkage::Local, &sig)
257+ .unwrap();
258+ self.user_fns.insert(name.clone(), (func_id, n_params));
259+ }
260+ _ => {}
261+ }
262+ }
263+ }
264+
265+ // ── Function definitions ──────────────────────────────────────
266+
267+ fn define_functions(&mut self, module: &ast::Module, _type_env: &TypeEnv) {
268+ for decl in &module.decls {
269+ if let ast::DeclKind::Fun {
270+ name,
271+ params,
272+ body,
273+ ..
274+ } = &decl.node
275+ {
276+ self.define_user_function(name, params, body);
277+ }
278+ }
279+
280+ // Compile any pending lambdas (may generate more lambdas)
281+ while !self.pending_lambdas.is_empty() {
282+ let lambdas: Vec<PendingLambda> =
283+ std::mem::take(&mut self.pending_lambdas);
284+ for lambda in lambdas {
285+ self.define_lambda_function(&lambda);
286+ }
287+ }
288+ }
289+
290+ /// Temporarily move ctx/builder_ctx out of self so the FunctionBuilder
291+ /// doesn't borrow self, allowing self.method() calls during building.
292+ fn build_function<F>(&mut self, func_id: FuncId, sig: cranelift_codegen::ir::Signature, f: F)
293+ where
294+ F: FnOnce(&mut Self, &mut FunctionBuilder, cranelift_codegen::ir::Block),
295+ {
296+ let mut ctx = std::mem::replace(&mut self.ctx, self.module.make_context());
297+ let mut fb_ctx =
298+ std::mem::replace(&mut self.builder_ctx, FunctionBuilderContext::new());
299+
300+ ctx.func.signature = sig;
301+
302+ {
303+ let mut builder = FunctionBuilder::new(&mut ctx.func, &mut fb_ctx);
304+ let entry = builder.create_block();
305+ builder.append_block_params_for_function_params(entry);
306+ builder.switch_to_block(entry);
307+ builder.seal_block(entry);
308+
309+ f(self, &mut builder, entry);
310+
311+ builder.finalize();
312+ }
313+
314+ self.module.define_function(func_id, &mut ctx).unwrap();
315+ self.builder_ctx = fb_ctx;
316+ self.ctx = ctx;
317+ self.module.clear_context(&mut self.ctx);
318+ }
319+
320+ fn define_user_function(
321+ &mut self,
322+ name: &str,
323+ params: &[ast::Pat],
324+ body: &ast::Expr,
325+ ) {
326+ let (func_id, _) = self.user_fns[name];
327+ let mut sig = self.module.make_signature();
328+ sig.params.push(AbiParam::new(self.ptr_type)); // db
329+ for _ in params {
330+ sig.params.push(AbiParam::new(self.ptr_type));
331+ }
332+ sig.returns.push(AbiParam::new(self.ptr_type));
333+
334+ let params_owned: Vec<ast::Pat> = params.to_vec();
335+ let body_owned = body.clone();
336+
337+ self.build_function(func_id, sig, |cg, builder, entry| {
338+ let mut env = Env::new();
339+ let db = builder.block_params(entry)[0];
340+ for (i, pat) in params_owned.iter().enumerate() {
341+ let val = builder.block_params(entry)[i + 1];
342+ bind_pattern_env(pat, val, &mut env);
343+ }
344+
345+ let result = cg.compile_expr(builder, &body_owned, &mut env, db);
346+ builder.ins().return_(&[result]);
347+ });
348+ }
349+
350+ fn define_lambda_function(&mut self, lambda: &PendingLambda) {
351+ let mut sig = self.module.make_signature();
352+ sig.params.push(AbiParam::new(self.ptr_type)); // db
353+ sig.params.push(AbiParam::new(self.ptr_type)); // env
354+ sig.params.push(AbiParam::new(self.ptr_type)); // arg
355+ sig.returns.push(AbiParam::new(self.ptr_type));
356+
357+ let func_id = lambda.func_id;
358+ let params = lambda.params.clone();
359+ let body = lambda.body.clone();
360+ let free_vars = lambda.free_vars.clone();
361+
362+ self.build_function(func_id, sig, |cg, builder, entry| {
363+ let mut env = Env::new();
364+ let db = builder.block_params(entry)[0];
365+ let closure_env = builder.block_params(entry)[1];
366+ let arg = builder.block_params(entry)[2];
367+
368+ // Unpack free variables from closure env
369+ for var_name in &free_vars {
370+ let (key_ptr, key_len) = cg.string_ptr(builder, var_name);
371+ let field_val =
372+ cg.call_rt(builder, "knot_record_field", &[closure_env, key_ptr, key_len]);
373+ env.set(var_name, field_val);
374+ }
375+
376+ // Bind parameter
377+ if params.len() == 1 {
378+ env.set(&params[0], arg);
379+ }
380+
381+ let result = cg.compile_expr(builder, &body, &mut env, db);
382+ builder.ins().return_(&[result]);
383+ });
384+ }
385+
386+ // ── Main function generation ──────────────────────────────────
387+
388+ fn generate_main(&mut self, module: &ast::Module) {
389+ let mut sig = self.module.make_signature();
390+ sig.returns.push(AbiParam::new(types::I32));
391+ let main_id = self
392+ .module
393+ .declare_function("main", Linkage::Export, &sig)
394+ .unwrap();
395+
396+ let decls: Vec<ast::Decl> = module.decls.clone();
397+ let user_main = self.user_fns.get("main").copied();
398+
399+ self.build_function(main_id, sig, |cg, builder, _entry| {
400+ // Open database
401+ let (db_path_ptr, db_path_len) = cg.string_ptr(builder, "knot.db");
402+ let db_open_ref = cg.import_rt(builder, "knot_db_open");
403+ let db_open_call =
404+ builder.ins().call(db_open_ref, &[db_path_ptr, db_path_len]);
405+ let db = builder.inst_results(db_open_call)[0];
406+
407+ // Initialize source tables
408+ for decl in &decls {
409+ if let ast::DeclKind::Source { name, .. } = &decl.node {
410+ let schema = cg
411+ .source_schemas
412+ .get(name)
413+ .cloned()
414+ .unwrap_or_default();
415+ let (name_ptr, name_len) = cg.string_ptr(builder, name);
416+ let (schema_ptr, schema_len) = cg.string_ptr(builder, &schema);
417+ let init_ref = cg.import_rt(builder, "knot_source_init");
418+ builder.ins().call(
419+ init_ref,
420+ &[db, name_ptr, name_len, schema_ptr, schema_len],
421+ );
422+ }
423+ }
424+
425+ // Call user's main function if it exists
426+ if let Some((main_fn_id, n_params)) = user_main {
427+ if n_params == 0 {
428+ let user_main_ref =
429+ cg.module.declare_func_in_func(main_fn_id, builder.func);
430+ let call = builder.ins().call(user_main_ref, &[db]);
431+ let result = builder.inst_results(call)[0];
432+
433+ // Print the result
434+ let println_ref = cg.import_rt(builder, "knot_println");
435+ builder.ins().call(println_ref, &[result]);
436+ }
437+ }
438+
439+ // Close database
440+ let db_close_ref = cg.import_rt(builder, "knot_db_close");
441+ builder.ins().call(db_close_ref, &[db]);
442+
443+ let zero = builder.ins().iconst(types::I32, 0);
444+ builder.ins().return_(&[zero]);
445+ });
446+ }
447+
448+ // ── Finish ────────────────────────────────────────────────────
449+
450+ fn finish(self) -> Vec<u8> {
451+ let product = self.module.finish();
452+ product.emit().unwrap()
453+ }
454+
455+ // ── Expression compilation ────────────────────────────────────
456+
457+ fn compile_expr(
458+ &mut self,
459+ builder: &mut FunctionBuilder,
460+ expr: &ast::Expr,
461+ env: &mut Env,
462+ db: Value,
463+ ) -> Value {
464+ match &expr.node {
465+ ast::ExprKind::Lit(lit) => self.compile_lit(builder, lit),
466+
467+ ast::ExprKind::Var(name) => {
468+ if env.bindings.contains_key(name) {
469+ env.get(name)
470+ } else if let Some((func_id, _n_params)) =
471+ self.user_fns.get(name).copied()
472+ {
473+ // Create a function value wrapping the user function
474+ let func_ref =
475+ self.module.declare_func_in_func(func_id, builder.func);
476+ let fn_addr = builder.ins().func_addr(self.ptr_type, func_ref);
477+ let null = builder.ins().iconst(self.ptr_type, 0);
478+ let mk_fn = self.import_rt(builder, "knot_value_function");
479+ let call = builder.ins().call(mk_fn, &[fn_addr, null]);
480+ builder.inst_results(call)[0]
481+ } else {
482+ panic!("codegen: undefined variable '{}'", name)
483+ }
484+ }
485+
486+ ast::ExprKind::Constructor(name) => {
487+ // Bare constructor reference — return as a unit constructor
488+ let (tag_ptr, tag_len) = self.string_ptr(builder, name);
489+ let unit = self.call_rt(builder, "knot_value_unit", &[]);
490+ let ctor =
491+ self.call_rt(builder, "knot_value_constructor", &[tag_ptr, tag_len, unit]);
492+ ctor
493+ }
494+
495+ ast::ExprKind::SourceRef(name) => {
496+ let schema = self
497+ .source_schemas
498+ .get(name)
499+ .cloned()
500+ .unwrap_or_default();
501+ let (name_ptr, name_len) = self.string_ptr(builder, name);
502+ let (schema_ptr, schema_len) = self.string_ptr(builder, &schema);
503+ self.call_rt(
504+ builder,
505+ "knot_source_read",
506+ &[db, name_ptr, name_len, schema_ptr, schema_len],
507+ )
508+ }
509+
510+ ast::ExprKind::DerivedRef(name) => {
511+ // For now, treat derived refs like function calls
512+ if let Some((func_id, 0)) = self.user_fns.get(name).copied() {
513+ let func_ref =
514+ self.module.declare_func_in_func(func_id, builder.func);
515+ let call = builder.ins().call(func_ref, &[db]);
516+ builder.inst_results(call)[0]
517+ } else {
518+ panic!("codegen: undefined derived relation '&{}'", name)
519+ }
520+ }
521+
522+ ast::ExprKind::Record(fields) => {
523+ let n = fields.len();
524+ let n_val = builder.ins().iconst(self.ptr_type, n as i64);
525+ let record = self.call_rt(builder, "knot_record_empty", &[n_val]);
526+ for field in fields {
527+ let val = self.compile_expr(builder, &field.value, env, db);
528+ let (key_ptr, key_len) = self.string_ptr(builder, &field.name);
529+ self.call_rt_void(
530+ builder,
531+ "knot_record_set_field",
532+ &[record, key_ptr, key_len, val],
533+ );
534+ }
535+ record
536+ }
537+
538+ ast::ExprKind::RecordUpdate { base, fields } => {
539+ let base_val = self.compile_expr(builder, base, env, db);
540+ let updated = self.call_rt(builder, "knot_record_update", &[base_val]);
541+ for field in fields {
542+ let val = self.compile_expr(builder, &field.value, env, db);
543+ let (key_ptr, key_len) = self.string_ptr(builder, &field.name);
544+ self.call_rt_void(
545+ builder,
546+ "knot_record_set_field",
547+ &[updated, key_ptr, key_len, val],
548+ );
549+ }
550+ updated
551+ }
552+
553+ ast::ExprKind::FieldAccess { expr, field } => {
554+ let val = self.compile_expr(builder, expr, env, db);
555+ let (key_ptr, key_len) = self.string_ptr(builder, field);
556+ self.call_rt(builder, "knot_record_field", &[val, key_ptr, key_len])
557+ }
558+
559+ ast::ExprKind::List(elems) => {
560+ let rel = self.call_rt(builder, "knot_relation_empty", &[]);
561+ for elem in elems {
562+ let val = self.compile_expr(builder, elem, env, db);
563+ self.call_rt_void(builder, "knot_relation_push", &[rel, val]);
564+ }
565+ rel
566+ }
567+
568+ ast::ExprKind::BinOp { op, lhs, rhs } => {
569+ if matches!(op, ast::BinOp::Pipe) {
570+ // lhs |> rhs => rhs(lhs)
571+ let arg = self.compile_expr(builder, lhs, env, db);
572+ let func = self.compile_expr(builder, rhs, env, db);
573+ self.call_rt(builder, "knot_value_call", &[db, func, arg])
574+ } else {
575+ let l = self.compile_expr(builder, lhs, env, db);
576+ let r = self.compile_expr(builder, rhs, env, db);
577+ let fn_name = match op {
578+ ast::BinOp::Add => "knot_value_add",
579+ ast::BinOp::Sub => "knot_value_sub",
580+ ast::BinOp::Mul => "knot_value_mul",
581+ ast::BinOp::Div => "knot_value_div",
582+ ast::BinOp::Eq => "knot_value_eq",
583+ ast::BinOp::Neq => "knot_value_neq",
584+ ast::BinOp::Lt => "knot_value_lt",
585+ ast::BinOp::Gt => "knot_value_gt",
586+ ast::BinOp::Le => "knot_value_le",
587+ ast::BinOp::Ge => "knot_value_ge",
588+ ast::BinOp::And => "knot_value_and",
589+ ast::BinOp::Or => "knot_value_or",
590+ ast::BinOp::Concat => "knot_value_concat",
591+ ast::BinOp::Pipe => unreachable!(),
592+ };
593+ self.call_rt(builder, fn_name, &[l, r])
594+ }
595+ }
596+
597+ ast::ExprKind::UnaryOp { op, operand } => {
598+ let val = self.compile_expr(builder, operand, env, db);
599+ let fn_name = match op {
600+ ast::UnaryOp::Neg => "knot_value_negate",
601+ ast::UnaryOp::Not => "knot_value_not",
602+ };
603+ self.call_rt(builder, fn_name, &[val])
604+ }
605+
606+ ast::ExprKind::If {
607+ cond,
608+ then_branch,
609+ else_branch,
610+ } => {
611+ let cond_val = self.compile_expr(builder, cond, env, db);
612+ let bool_val =
613+ self.call_rt_typed(builder, "knot_value_get_bool", &[cond_val], types::I32);
614+ let is_true =
615+ builder.ins().icmp_imm(IntCC::NotEqual, bool_val, 0);
616+
617+ let then_block = builder.create_block();
618+ let else_block = builder.create_block();
619+ let merge_block = builder.create_block();
620+ merge_block_param(builder, merge_block, self.ptr_type);
621+
622+ builder
623+ .ins()
624+ .brif(is_true, then_block, &[], else_block, &[]);
625+
626+ builder.switch_to_block(then_block);
627+ builder.seal_block(then_block);
628+ let then_val =
629+ self.compile_expr(builder, then_branch, &mut env.clone(), db);
630+ builder.ins().jump(merge_block, &[then_val]);
631+
632+ builder.switch_to_block(else_block);
633+ builder.seal_block(else_block);
634+ let else_val =
635+ self.compile_expr(builder, else_branch, &mut env.clone(), db);
636+ builder.ins().jump(merge_block, &[else_val]);
637+
638+ builder.switch_to_block(merge_block);
639+ builder.seal_block(merge_block);
640+ builder.block_params(merge_block)[0]
641+ }
642+
643+ ast::ExprKind::Lambda { params, body } => {
644+ self.compile_lambda(builder, params, body, env, db)
645+ }
646+
647+ ast::ExprKind::App { func: _, arg: _ } => {
648+ self.compile_app(builder, expr, env, db)
649+ }
650+
651+ ast::ExprKind::Do(stmts) => self.compile_do(builder, stmts, env, db),
652+
653+ ast::ExprKind::Yield(inner) => {
654+ let val = self.compile_expr(builder, inner, env, db);
655+ self.call_rt(builder, "knot_relation_singleton", &[val])
656+ }
657+
658+ ast::ExprKind::Set { target, value } => {
659+ // target should be a SourceRef
660+ if let ast::ExprKind::SourceRef(name) = &target.node {
661+ let schema = self
662+ .source_schemas
663+ .get(name)
664+ .cloned()
665+ .unwrap_or_default();
666+ let val = self.compile_expr(builder, value, env, db);
667+ let (name_ptr, name_len) = self.string_ptr(builder, name);
668+ let (schema_ptr, schema_len) =
669+ self.string_ptr(builder, &schema);
670+ self.call_rt_void(
671+ builder,
672+ "knot_source_write",
673+ &[db, name_ptr, name_len, schema_ptr, schema_len, val],
674+ );
675+ self.call_rt(builder, "knot_value_unit", &[])
676+ } else {
677+ panic!("codegen: set target must be a source reference")
678+ }
679+ }
680+
681+ ast::ExprKind::Atomic(inner) => {
682+ self.call_rt_void(builder, "knot_atomic_begin", &[db]);
683+ let val = self.compile_expr(builder, inner, env, db);
684+ self.call_rt_void(builder, "knot_atomic_commit", &[db]);
685+ val
686+ }
687+
688+ ast::ExprKind::Case {
689+ scrutinee,
690+ arms,
691+ } => self.compile_case(builder, scrutinee, arms, env, db),
692+
693+ ast::ExprKind::At { .. } => {
694+ // Temporal queries not yet supported
695+ self.call_rt(builder, "knot_value_unit", &[])
696+ }
697+ }
698+ }
699+
700+ // ── Application compilation ───────────────────────────────────
701+
702+ fn compile_app(
703+ &mut self,
704+ builder: &mut FunctionBuilder,
705+ expr: &ast::Expr,
706+ env: &mut Env,
707+ db: Value,
708+ ) -> Value {
709+ // Uncurry nested applications
710+ let (func_expr, args) = uncurry_app(expr);
711+ let compiled_args: Vec<Value> = args
712+ .iter()
713+ .map(|a| self.compile_expr(builder, a, env, db))
714+ .collect();
715+
716+ match &func_expr.node {
717+ // Direct call to a known user function
718+ ast::ExprKind::Var(name)
719+ if self.user_fns.contains_key(name) =>
720+ {
721+ let (func_id, expected_params) =
722+ self.user_fns[name];
723+ if compiled_args.len() == expected_params {
724+ let func_ref = self
725+ .module
726+ .declare_func_in_func(func_id, builder.func);
727+ let mut call_args = vec![db];
728+ call_args.extend(&compiled_args);
729+ let call = builder.ins().call(func_ref, &call_args);
730+ builder.inst_results(call)[0]
731+ } else {
732+ // Partial application or over-application — use dynamic call
733+ let func_val =
734+ self.compile_expr(builder, func_expr, env, db);
735+ let mut result = func_val;
736+ for arg in &compiled_args {
737+ result = self.call_rt(
738+ builder,
739+ "knot_value_call",
740+ &[db, result, *arg],
741+ );
742+ }
743+ result
744+ }
745+ }
746+
747+ // Built-in functions
748+ ast::ExprKind::Var(name) if name == "println" || name == "putLine" => {
749+ let rt_name = "knot_println";
750+ if compiled_args.len() == 1 {
751+ self.call_rt(builder, rt_name, &[compiled_args[0]])
752+ } else {
753+ self.call_rt(builder, "knot_value_unit", &[])
754+ }
755+ }
756+ ast::ExprKind::Var(name) if name == "print" => {
757+ if compiled_args.len() == 1 {
758+ self.call_rt(builder, "knot_print", &[compiled_args[0]])
759+ } else {
760+ self.call_rt(builder, "knot_value_unit", &[])
761+ }
762+ }
763+ ast::ExprKind::Var(name) if name == "show" => {
764+ // show is identity for now (values are already printable)
765+ if compiled_args.len() == 1 {
766+ compiled_args[0]
767+ } else {
768+ self.call_rt(builder, "knot_value_unit", &[])
769+ }
770+ }
771+ ast::ExprKind::Var(name) if name == "union" => {
772+ if compiled_args.len() == 2 {
773+ self.call_rt(
774+ builder,
775+ "knot_relation_union",
776+ &[compiled_args[0], compiled_args[1]],
777+ )
778+ } else {
779+ self.call_rt(builder, "knot_value_unit", &[])
780+ }
781+ }
782+ ast::ExprKind::Var(name) if name == "count" => {
783+ if compiled_args.len() == 1 {
784+ // knot_relation_len returns raw usize, pass directly to knot_value_int
785+ let len =
786+ self.call_rt(builder, "knot_relation_len", &[compiled_args[0]]);
787+ self.call_rt(builder, "knot_value_int", &[len])
788+ } else {
789+ self.call_rt(builder, "knot_value_unit", &[])
790+ }
791+ }
792+
793+ // Constructor application: `Circle {radius: 3.14}`
794+ ast::ExprKind::Constructor(name) => {
795+ let (tag_ptr, tag_len) = self.string_ptr(builder, name);
796+ let payload = if compiled_args.len() == 1 {
797+ compiled_args[0]
798+ } else {
799+ self.call_rt(builder, "knot_value_unit", &[])
800+ };
801+ self.call_rt(
802+ builder,
803+ "knot_value_constructor",
804+ &[tag_ptr, tag_len, payload],
805+ )
806+ }
807+
808+ // Dynamic call through a function value
809+ _ => {
810+ let func_val =
811+ self.compile_expr(builder, func_expr, env, db);
812+ let mut result = func_val;
813+ for arg in &compiled_args {
814+ result = self.call_rt(
815+ builder,
816+ "knot_value_call",
817+ &[db, result, *arg],
818+ );
819+ }
820+ result
821+ }
822+ }
823+ }
824+
825+ // ── Case expression compilation ───────────────────────────────
826+
827+ fn compile_case(
828+ &mut self,
829+ builder: &mut FunctionBuilder,
830+ scrutinee: &ast::Expr,
831+ arms: &[ast::CaseArm],
832+ env: &mut Env,
833+ db: Value,
834+ ) -> Value {
835+ let scrut = self.compile_expr(builder, scrutinee, env, db);
836+ let merge_block = builder.create_block();
837+ merge_block_param(builder, merge_block, self.ptr_type);
838+
839+ for (i, arm) in arms.iter().enumerate() {
840+ let is_last = i == arms.len() - 1;
841+ let arm_block = builder.create_block();
842+ let next_block = if is_last {
843+ merge_block
844+ } else {
845+ builder.create_block()
846+ };
847+
848+ // Test the pattern
849+ match &arm.pat.node {
850+ ast::PatKind::Wildcard | ast::PatKind::Var(_) => {
851+ // Always matches
852+ builder.ins().jump(arm_block, &[]);
853+ }
854+ ast::PatKind::Constructor { name, .. } => {
855+ let (tag_ptr, tag_len) = self.string_ptr(builder, name);
856+ let matches = self.call_rt_typed(
857+ builder,
858+ "knot_constructor_matches",
859+ &[scrut, tag_ptr, tag_len],
860+ types::I32,
861+ );
862+ let is_match =
863+ builder.ins().icmp_imm(IntCC::NotEqual, matches, 0);
864+ builder
865+ .ins()
866+ .brif(is_match, arm_block, &[], next_block, &[]);
867+ }
868+ ast::PatKind::Lit(lit) => {
869+ let lit_val = self.compile_lit(builder, lit);
870+ let eq =
871+ self.call_rt(builder, "knot_value_eq", &[scrut, lit_val]);
872+ let eq_bool = self.call_rt_typed(
873+ builder,
874+ "knot_value_get_bool",
875+ &[eq],
876+ types::I32,
877+ );
878+ let is_eq =
879+ builder.ins().icmp_imm(IntCC::NotEqual, eq_bool, 0);
880+ builder
881+ .ins()
882+ .brif(is_eq, arm_block, &[], next_block, &[]);
883+ }
884+ _ => {
885+ // Default: try matching
886+ builder.ins().jump(arm_block, &[]);
887+ }
888+ }
889+
890+ builder.switch_to_block(arm_block);
891+ builder.seal_block(arm_block);
892+
893+ // Bind pattern variables
894+ let mut arm_env = env.clone();
895+ self.bind_case_pattern(builder, &arm.pat, scrut, &mut arm_env);
896+
897+ let arm_val =
898+ self.compile_expr(builder, &arm.body, &mut arm_env, db);
899+ builder.ins().jump(merge_block, &[arm_val]);
900+
901+ if !is_last {
902+ builder.switch_to_block(next_block);
903+ builder.seal_block(next_block);
904+ }
905+ }
906+
907+ builder.switch_to_block(merge_block);
908+ builder.seal_block(merge_block);
909+ builder.block_params(merge_block)[0]
910+ }
911+
912+ fn bind_case_pattern(
913+ &mut self,
914+ builder: &mut FunctionBuilder,
915+ pat: &ast::Pat,
916+ val: Value,
917+ env: &mut Env,
918+ ) {
919+ match &pat.node {
920+ ast::PatKind::Var(name) => env.set(name, val),
921+ ast::PatKind::Wildcard => {}
922+ ast::PatKind::Constructor { name: _, payload } => {
923+ let inner = self.call_rt(builder, "knot_constructor_payload", &[val]);
924+ self.bind_case_pattern(builder, payload, inner, env);
925+ }
926+ ast::PatKind::Record(fields) => {
927+ for fp in fields {
928+ let (key_ptr, key_len) = self.string_ptr(builder, &fp.name);
929+ let field_val =
930+ self.call_rt(builder, "knot_record_field", &[val, key_ptr, key_len]);
931+ if let Some(inner_pat) = &fp.pattern {
932+ self.bind_case_pattern(builder, inner_pat, field_val, env);
933+ } else {
934+ // Punned: {name} means {name: name}
935+ env.set(&fp.name, field_val);
936+ }
937+ }
938+ }
939+ ast::PatKind::Lit(_) => {
940+ // Literal patterns don't bind anything
941+ }
942+ ast::PatKind::List(_) => {
943+ // List pattern not yet implemented in codegen
944+ }
945+ }
946+ }
947+
948+ // ── Do-block compilation ──────────────────────────────────────
949+
950+ fn compile_do(
951+ &mut self,
952+ builder: &mut FunctionBuilder,
953+ stmts: &[ast::Stmt],
954+ env: &mut Env,
955+ db: Value,
956+ ) -> Value {
957+ let result = self.call_rt(builder, "knot_relation_empty", &[]);
958+ let mut loop_stack: Vec<LoopInfo> = Vec::new();
959+
960+ for (stmt_idx, stmt) in stmts.iter().enumerate() {
961+ match &stmt.node {
962+ ast::StmtKind::Bind { pat, expr } => {
963+ let rel = self.compile_expr(builder, expr, env, db);
964+ // knot_relation_len returns a raw usize, not a boxed Value
965+ let len = self.call_rt(builder, "knot_relation_len", &[rel]);
966+
967+ let header = builder.create_block();
968+ let body = builder.create_block();
969+ let continue_blk = builder.create_block();
970+ let exit = builder.create_block();
971+
972+ let zero = builder.ins().iconst(types::I64, 0);
973+ builder.ins().jump(header, &[zero]);
974+
975+ builder.switch_to_block(header);
976+ let i = builder.append_block_param(header, types::I64);
977+ let cond =
978+ builder.ins().icmp(IntCC::SignedLessThan, i, len);
979+ builder.ins().brif(cond, body, &[], exit, &[]);
980+
981+ builder.switch_to_block(body);
982+ builder.seal_block(body);
983+
984+ let row = self.call_rt(builder, "knot_relation_get", &[rel, i]);
985+
986+ // Bind pattern
987+ bind_do_pattern(builder, self, pat, row, env);
988+
989+ loop_stack.push(LoopInfo {
990+ header,
991+ continue_blk,
992+ exit,
993+ index_var: i,
994+ where_skips: Vec::new(),
995+ });
996+ }
997+
998+ ast::StmtKind::Where { cond } => {
999+ let cond_val = self.compile_expr(builder, cond, env, db);
1000+ let bool_val = self.call_rt_typed(
1001+ builder,
1002+ "knot_value_get_bool",
1003+ &[cond_val],
1004+ types::I32,
1005+ );
1006+ let is_true =
1007+ builder.ins().icmp_imm(IntCC::NotEqual, bool_val, 0);
1008+
1009+ let then_block = builder.create_block();
1010+ let skip_block = builder.create_block();
1011+ builder
1012+ .ins()
1013+ .brif(is_true, then_block, &[], skip_block, &[]);
1014+
1015+ builder.switch_to_block(then_block);
1016+ builder.seal_block(then_block);
1017+
1018+ if let Some(loop_info) = loop_stack.last_mut() {
1019+ loop_info.where_skips.push(skip_block);
1020+ } else {
1021+ // Where outside a loop — just seal the skip block
1022+ builder.switch_to_block(skip_block);
1023+ builder.seal_block(skip_block);
1024+ // Switch back — this is a degenerate case
1025+ }
1026+ }
1027+
1028+ ast::StmtKind::Let { pat, expr } => {
1029+ let val = self.compile_expr(builder, expr, env, db);
1030+ bind_pattern_env(pat, val, env);
1031+ }
1032+
1033+ ast::StmtKind::Expr(expr) => {
1034+ let is_last = stmt_idx == stmts.len() - 1;
1035+ match &expr.node {
1036+ ast::ExprKind::Yield(inner) => {
1037+ let val =
1038+ self.compile_expr(builder, inner, env, db);
1039+ self.call_rt_void(
1040+ builder,
1041+ "knot_relation_push",
1042+ &[result, val],
1043+ );
1044+ }
1045+ ast::ExprKind::Set { target: _, value: _ } => {
1046+ // Compile set inside do block
1047+ let _ = self.compile_expr(builder, expr, env, db);
1048+ }
1049+ _ => {
1050+ let val =
1051+ self.compile_expr(builder, expr, env, db);
1052+ if is_last && loop_stack.is_empty() {
1053+ // Last expression in a non-looping do block
1054+ // — push as result
1055+ self.call_rt_void(
1056+ builder,
1057+ "knot_relation_push",
1058+ &[result, val],
1059+ );
1060+ }
1061+ }
1062+ }
1063+ }
1064+ }
1065+ }
1066+
1067+ // Close loops from innermost to outermost
1068+ while let Some(info) = loop_stack.pop() {
1069+ // From current block, jump to continue_blk
1070+ builder.ins().jump(info.continue_blk, &[]);
1071+
1072+ // Handle where skips
1073+ for skip in &info.where_skips {
1074+ builder.switch_to_block(*skip);
1075+ builder.seal_block(*skip);
1076+ builder.ins().jump(info.continue_blk, &[]);
1077+ }
1078+
1079+ // Continue block: increment and loop back
1080+ builder.switch_to_block(info.continue_blk);
1081+ builder.seal_block(info.continue_blk);
1082+ let one = builder.ins().iconst(types::I64, 1);
1083+ let next_i = builder.ins().iadd(info.index_var, one);
1084+ builder.ins().jump(info.header, &[next_i]);
1085+
1086+ // Seal header (all predecessors now known)
1087+ builder.seal_block(info.header);
1088+
1089+ // Switch to exit block for the next outer loop
1090+ builder.switch_to_block(info.exit);
1091+ builder.seal_block(info.exit);
1092+ }
1093+
1094+ result
1095+ }
1096+
1097+ // ── Lambda compilation ────────────────────────────────────────
1098+
1099+ fn compile_lambda(
1100+ &mut self,
1101+ builder: &mut FunctionBuilder,
1102+ params: &[ast::Pat],
1103+ body: &ast::Expr,
1104+ env: &mut Env,
1105+ _db: Value,
1106+ ) -> Value {
1107+ let lambda_name = format!("knot_lambda_{}", self.lambda_counter);
1108+ self.lambda_counter += 1;
1109+
1110+ // Determine free variables
1111+ let param_names: Vec<String> = params
1112+ .iter()
1113+ .filter_map(|p| match &p.node {
1114+ ast::PatKind::Var(name) => Some(name.clone()),
1115+ _ => None,
1116+ })
1117+ .collect();
1118+ let free_vars = find_free_vars(body, &param_names);
1119+
1120+ // Declare the lambda function: (db, env, arg) -> result
1121+ let mut sig = self.module.make_signature();
1122+ sig.params.push(AbiParam::new(self.ptr_type)); // db
1123+ sig.params.push(AbiParam::new(self.ptr_type)); // env
1124+ sig.params.push(AbiParam::new(self.ptr_type)); // arg
1125+ sig.returns.push(AbiParam::new(self.ptr_type));
1126+ let func_id = self
1127+ .module
1128+ .declare_function(&lambda_name, Linkage::Local, &sig)
1129+ .unwrap();
1130+
1131+ // Queue for later compilation
1132+ self.pending_lambdas.push(PendingLambda {
1133+ func_id,
1134+ params: param_names.clone(),
1135+ body: body.clone(),
1136+ free_vars: free_vars.clone(),
1137+ });
1138+
1139+ // Build the closure: capture free variables into a record
1140+ let func_ref = self.module.declare_func_in_func(func_id, builder.func);
1141+ let fn_addr = builder.ins().func_addr(self.ptr_type, func_ref);
1142+
1143+ let env_val = if free_vars.is_empty() {
1144+ builder.ins().iconst(self.ptr_type, 0) // null env
1145+ } else {
1146+ let n = free_vars.len();
1147+ let n_val = builder.ins().iconst(self.ptr_type, n as i64);
1148+ let env_record = self.call_rt(builder, "knot_record_empty", &[n_val]);
1149+ for var_name in &free_vars {
1150+ let val = env.get(var_name);
1151+ let (key_ptr, key_len) = self.string_ptr(builder, var_name);
1152+ self.call_rt_void(
1153+ builder,
1154+ "knot_record_set_field",
1155+ &[env_record, key_ptr, key_len, val],
1156+ );
1157+ }
1158+ env_record
1159+ };
1160+
1161+ self.call_rt(builder, "knot_value_function", &[fn_addr, env_val])
1162+ }
1163+
1164+ // ── Literal compilation ───────────────────────────────────────
1165+
1166+ fn compile_lit(
1167+ &mut self,
1168+ builder: &mut FunctionBuilder,
1169+ lit: &ast::Literal,
1170+ ) -> Value {
1171+ match lit {
1172+ ast::Literal::Int(n) => {
1173+ let n_val = builder.ins().iconst(types::I64, *n);
1174+ self.call_rt(builder, "knot_value_int", &[n_val])
1175+ }
1176+ ast::Literal::Float(n) => {
1177+ let n_val = builder.ins().f64const(*n);
1178+ self.call_rt(builder, "knot_value_float", &[n_val])
1179+ }
1180+ ast::Literal::Text(s) => {
1181+ let (ptr, len) = self.string_ptr(builder, s);
1182+ self.call_rt(builder, "knot_value_text", &[ptr, len])
1183+ }
1184+ }
1185+ }
1186+
1187+ // ── Runtime call helpers ──────────────────────────────────────
1188+
1189+ /// Call a runtime function that returns a pointer-typed value.
1190+ fn call_rt(
1191+ &mut self,
1192+ builder: &mut FunctionBuilder,
1193+ name: &str,
1194+ args: &[Value],
1195+ ) -> Value {
1196+ let func_id = self.runtime_fns[name];
1197+ let func_ref = self.module.declare_func_in_func(func_id, builder.func);
1198+ let call = builder.ins().call(func_ref, args);
1199+ builder.inst_results(call)[0]
1200+ }
1201+
1202+ /// Call a runtime function that returns a specific type.
1203+ fn call_rt_typed(
1204+ &mut self,
1205+ builder: &mut FunctionBuilder,
1206+ name: &str,
1207+ args: &[Value],
1208+ _ret_type: types::Type,
1209+ ) -> Value {
1210+ let func_id = self.runtime_fns[name];
1211+ let func_ref = self.module.declare_func_in_func(func_id, builder.func);
1212+ let call = builder.ins().call(func_ref, args);
1213+ builder.inst_results(call)[0]
1214+ }
1215+
1216+ /// Call a runtime function that returns void.
1217+ fn call_rt_void(
1218+ &mut self,
1219+ builder: &mut FunctionBuilder,
1220+ name: &str,
1221+ args: &[Value],
1222+ ) {
1223+ let func_id = self.runtime_fns[name];
1224+ let func_ref = self.module.declare_func_in_func(func_id, builder.func);
1225+ builder.ins().call(func_ref, args);
1226+ }
1227+
1228+ /// Import a runtime function reference into the current function.
1229+ fn import_rt(
1230+ &mut self,
1231+ builder: &mut FunctionBuilder,
1232+ name: &str,
1233+ ) -> cranelift_codegen::ir::FuncRef {
1234+ let func_id = self.runtime_fns[name];
1235+ self.module.declare_func_in_func(func_id, builder.func)
1236+ }
1237+
1238+ // ── String constant helpers ───────────────────────────────────
1239+
1240+ /// Ensure a string constant exists in the module data section.
1241+ /// Returns the DataId.
1242+ fn ensure_string(&mut self, s: &str) -> DataId {
1243+ if let Some(id) = self.strings.get(s) {
1244+ return *id;
1245+ }
1246+ let name = format!(".str.{}", self.string_counter);
1247+ self.string_counter += 1;
1248+ let data_id = self
1249+ .module
1250+ .declare_data(&name, Linkage::Local, false, false)
1251+ .unwrap();
1252+ let mut desc = DataDescription::new();
1253+ desc.define(s.as_bytes().to_vec().into_boxed_slice());
1254+ self.module.define_data(data_id, &desc).unwrap();
1255+ self.strings.insert(s.to_string(), data_id);
1256+ data_id
1257+ }
1258+
1259+ /// Get the pointer and length of a string constant as Cranelift Values.
1260+ fn string_ptr(
1261+ &mut self,
1262+ builder: &mut FunctionBuilder,
1263+ s: &str,
1264+ ) -> (Value, Value) {
1265+ let data_id = self.ensure_string(s);
1266+ let gv = self
1267+ .module
1268+ .declare_data_in_func(data_id, builder.func);
1269+ let ptr = builder.ins().global_value(self.ptr_type, gv);
1270+ let len = builder.ins().iconst(self.ptr_type, s.len() as i64);
1271+ (ptr, len)
1272+ }
1273+}
1274+
1275+// ── Free functions ────────────────────────────────────────────────
1276+
1277+fn merge_block_param(
1278+ builder: &mut FunctionBuilder,
1279+ block: cranelift_codegen::ir::Block,
1280+ ty: types::Type,
1281+) {
1282+ builder.append_block_param(block, ty);
1283+}
1284+
1285+fn bind_pattern_env(pat: &ast::Pat, val: Value, env: &mut Env) {
1286+ match &pat.node {
1287+ ast::PatKind::Var(name) => env.set(name, val),
1288+ ast::PatKind::Wildcard => {}
1289+ _ => {} // Simplified: only var and wildcard bindings for now
1290+ }
1291+}
1292+
1293+fn bind_do_pattern(
1294+ builder: &mut FunctionBuilder,
1295+ cg: &mut Codegen,
1296+ pat: &ast::Pat,
1297+ val: Value,
1298+ env: &mut Env,
1299+) {
1300+ match &pat.node {
1301+ ast::PatKind::Var(name) => env.set(name, val),
1302+ ast::PatKind::Wildcard => {}
1303+ ast::PatKind::Record(fields) => {
1304+ for fp in fields {
1305+ let (key_ptr, key_len) = cg.string_ptr(builder, &fp.name);
1306+ let field_val =
1307+ cg.call_rt(builder, "knot_record_field", &[val, key_ptr, key_len]);
1308+ if let Some(inner_pat) = &fp.pattern {
1309+ bind_do_pattern(builder, cg, inner_pat, field_val, env);
1310+ } else {
1311+ env.set(&fp.name, field_val);
1312+ }
1313+ }
1314+ }
1315+ ast::PatKind::Constructor { name: _, payload } => {
1316+ // Pattern match bind: `Circle c <- *shapes`
1317+ // This filters — only rows matching the constructor are bound
1318+ // For now, just extract the payload
1319+ let inner = cg.call_rt(builder, "knot_constructor_payload", &[val]);
1320+ bind_do_pattern(builder, cg, payload, inner, env);
1321+ }
1322+ _ => {}
1323+ }
1324+}
1325+
1326+/// Uncurry nested applications: `f x y` → `(f, [x, y])`.
1327+fn uncurry_app(expr: &ast::Expr) -> (&ast::Expr, Vec<&ast::Expr>) {
1328+ match &expr.node {
1329+ ast::ExprKind::App { func, arg } => {
1330+ let (f, mut args) = uncurry_app(func);
1331+ args.push(arg);
1332+ (f, args)
1333+ }
1334+ _ => (expr, Vec::new()),
1335+ }
1336+}
1337+
1338+/// Find free variables in an expression (variables not bound by params).
1339+fn find_free_vars(expr: &ast::Expr, bound: &[String]) -> Vec<String> {
1340+ let mut free = Vec::new();
1341+ collect_free_vars(expr, bound, &mut free);
1342+ free.sort();
1343+ free.dedup();
1344+ free
1345+}
1346+
1347+fn collect_free_vars(expr: &ast::Expr, bound: &[String], free: &mut Vec<String>) {
1348+ match &expr.node {
1349+ ast::ExprKind::Var(name) => {
1350+ if !bound.contains(name) && !is_builtin_name(name) {
1351+ free.push(name.clone());
1352+ }
1353+ }
1354+ ast::ExprKind::Lit(_) | ast::ExprKind::Constructor(_) => {}
1355+ ast::ExprKind::SourceRef(_) | ast::ExprKind::DerivedRef(_) => {}
1356+ ast::ExprKind::Record(fields) => {
1357+ for f in fields {
1358+ collect_free_vars(&f.value, bound, free);
1359+ }
1360+ }
1361+ ast::ExprKind::RecordUpdate { base, fields } => {
1362+ collect_free_vars(base, bound, free);
1363+ for f in fields {
1364+ collect_free_vars(&f.value, bound, free);
1365+ }
1366+ }
1367+ ast::ExprKind::FieldAccess { expr, .. } => {
1368+ collect_free_vars(expr, bound, free);
1369+ }
1370+ ast::ExprKind::List(elems) => {
1371+ for e in elems {
1372+ collect_free_vars(e, bound, free);
1373+ }
1374+ }
1375+ ast::ExprKind::Lambda { params, body } => {
1376+ let mut new_bound: Vec<String> = bound.to_vec();
1377+ for p in params {
1378+ if let ast::PatKind::Var(name) = &p.node {
1379+ new_bound.push(name.clone());
1380+ }
1381+ }
1382+ collect_free_vars(body, &new_bound, free);
1383+ }
1384+ ast::ExprKind::App { func, arg } => {
1385+ collect_free_vars(func, bound, free);
1386+ collect_free_vars(arg, bound, free);
1387+ }
1388+ ast::ExprKind::BinOp { lhs, rhs, .. } => {
1389+ collect_free_vars(lhs, bound, free);
1390+ collect_free_vars(rhs, bound, free);
1391+ }
1392+ ast::ExprKind::UnaryOp { operand, .. } => {
1393+ collect_free_vars(operand, bound, free);
1394+ }
1395+ ast::ExprKind::If {
1396+ cond,
1397+ then_branch,
1398+ else_branch,
1399+ } => {
1400+ collect_free_vars(cond, bound, free);
1401+ collect_free_vars(then_branch, bound, free);
1402+ collect_free_vars(else_branch, bound, free);
1403+ }
1404+ ast::ExprKind::Case { scrutinee, arms } => {
1405+ collect_free_vars(scrutinee, bound, free);
1406+ for arm in arms {
1407+ let mut arm_bound = bound.to_vec();
1408+ collect_pat_bindings(&arm.pat, &mut arm_bound);
1409+ collect_free_vars(&arm.body, &arm_bound, free);
1410+ }
1411+ }
1412+ ast::ExprKind::Do(stmts) => {
1413+ let mut do_bound = bound.to_vec();
1414+ for stmt in stmts {
1415+ match &stmt.node {
1416+ ast::StmtKind::Bind { pat, expr } => {
1417+ collect_free_vars(expr, &do_bound, free);
1418+ collect_pat_bindings(pat, &mut do_bound);
1419+ }
1420+ ast::StmtKind::Let { pat, expr } => {
1421+ collect_free_vars(expr, &do_bound, free);
1422+ collect_pat_bindings(pat, &mut do_bound);
1423+ }
1424+ ast::StmtKind::Where { cond } => {
1425+ collect_free_vars(cond, &do_bound, free);
1426+ }
1427+ ast::StmtKind::Expr(e) => {
1428+ collect_free_vars(e, &do_bound, free);
1429+ }
1430+ }
1431+ }
1432+ }
1433+ ast::ExprKind::Yield(inner) => {
1434+ collect_free_vars(inner, bound, free);
1435+ }
1436+ ast::ExprKind::Set { target, value } => {
1437+ collect_free_vars(target, bound, free);
1438+ collect_free_vars(value, bound, free);
1439+ }
1440+ ast::ExprKind::Atomic(inner) => {
1441+ collect_free_vars(inner, bound, free);
1442+ }
1443+ ast::ExprKind::At { relation, time } => {
1444+ collect_free_vars(relation, bound, free);
1445+ collect_free_vars(time, bound, free);
1446+ }
1447+ }
1448+}
1449+
1450+fn collect_pat_bindings(pat: &ast::Pat, bound: &mut Vec<String>) {
1451+ match &pat.node {
1452+ ast::PatKind::Var(name) => bound.push(name.clone()),
1453+ ast::PatKind::Wildcard => {}
1454+ ast::PatKind::Constructor { payload, .. } => {
1455+ collect_pat_bindings(payload, bound);
1456+ }
1457+ ast::PatKind::Record(fields) => {
1458+ for f in fields {
1459+ if let Some(p) = &f.pattern {
1460+ collect_pat_bindings(p, bound);
1461+ } else {
1462+ bound.push(f.name.clone());
1463+ }
1464+ }
1465+ }
1466+ ast::PatKind::Lit(_) => {}
1467+ ast::PatKind::List(pats) => {
1468+ for p in pats {
1469+ collect_pat_bindings(p, bound);
1470+ }
1471+ }
1472+ }
1473+}
1474+
1475+fn is_builtin_name(name: &str) -> bool {
1476+ matches!(
1477+ name,
1478+ "println"
1479+ | "putLine"
1480+ | "print"
1481+ | "show"
1482+ | "union"
1483+ | "count"
1484+ | "filter"
1485+ | "map"
1486+ | "fold"
1487+ )
1488+}
crates/knot-compiler/src/linker.rsnew file+36
@@ -0,0 +1,36 @@
1+//! Invokes the system linker to combine a Cranelift-generated object file
2+//! with the knot runtime static library into an executable.
3+
4+use std::path::Path;
5+use std::process::Command;
6+
7+pub fn link(
8+ object_path: &Path,
9+ runtime_path: &Path,
10+ output_path: &Path,
11+) -> Result<(), String> {
12+ let mut cmd = Command::new("cc");
13+ cmd.arg("-o")
14+ .arg(output_path)
15+ .arg(object_path)
16+ .arg(runtime_path);
17+
18+ // On macOS, link system libraries needed by the Rust runtime
19+ if cfg!(target_os = "macos") {
20+ cmd.arg("-lSystem").arg("-lresolv").arg("-liconv");
21+ } else {
22+ // Linux
23+ cmd.arg("-lpthread").arg("-ldl").arg("-lm");
24+ }
25+
26+ let output = cmd
27+ .output()
28+ .map_err(|e| format!("failed to run linker (cc): {}", e))?;
29+
30+ if !output.status.success() {
31+ let stderr = String::from_utf8_lossy(&output.stderr);
32+ return Err(format!("linker failed:\n{}", stderr));
33+ }
34+
35+ Ok(())
36+}
crates/knot-compiler/src/main.rsnew file+131
@@ -0,0 +1,131 @@
1+//! Knot compiler CLI.
2+//!
3+//! Usage: knotc build <file.knot>
4+
5+mod codegen;
6+mod linker;
7+mod types;
8+
9+use std::path::PathBuf;
10+use std::process;
11+
12+fn main() {
13+ let args: Vec<String> = std::env::args().collect();
14+
15+ if args.len() < 2 {
16+ print_usage();
17+ process::exit(1);
18+ }
19+
20+ match args[1].as_str() {
21+ "build" => {
22+ if args.len() < 3 {
23+ eprintln!("Error: missing source file");
24+ eprintln!("Usage: knotc build <file.knot>");
25+ process::exit(1);
26+ }
27+ cmd_build(&args[2]);
28+ }
29+ "--help" | "-h" | "help" => print_usage(),
30+ other => {
31+ eprintln!("Unknown command: {}", other);
32+ print_usage();
33+ process::exit(1);
34+ }
35+ }
36+}
37+
38+fn print_usage() {
39+ eprintln!("Knot compiler");
40+ eprintln!();
41+ eprintln!("Usage:");
42+ eprintln!(" knotc build <file.knot> Compile a Knot source file to an executable");
43+ eprintln!(" knotc help Show this help message");
44+}
45+
46+fn cmd_build(source_file: &str) {
47+ let source_path = PathBuf::from(source_file);
48+
49+ // Read source
50+ let source = std::fs::read_to_string(&source_path).unwrap_or_else(|e| {
51+ eprintln!("Error reading {}: {}", source_path.display(), e);
52+ process::exit(1);
53+ });
54+
55+ // Lex
56+ let lexer = knot::lexer::Lexer::new(&source);
57+ let (tokens, lex_diags) = lexer.tokenize();
58+ let filename = source_path.display().to_string();
59+ if !lex_diags.is_empty() {
60+ for diag in &lex_diags {
61+ eprintln!("{}", diag.render(&source, &filename));
62+ }
63+ }
64+
65+ // Parse
66+ let parser = knot::parser::Parser::new(source.clone(), tokens);
67+ let (module, parse_diags) = parser.parse_module();
68+ let has_errors = parse_diags
69+ .iter()
70+ .any(|d| d.severity == knot::diagnostic::Severity::Error);
71+ if has_errors {
72+ for diag in &parse_diags {
73+ eprintln!("{}", diag.render(&source, &filename));
74+ }
75+ process::exit(1);
76+ }
77+
78+ // Resolve types
79+ let type_env = types::TypeEnv::from_module(&module);
80+
81+ // Code generation
82+ let obj_bytes = codegen::compile(&module, &type_env);
83+
84+ // Write object file
85+ let obj_path = source_path.with_extension("o");
86+ std::fs::write(&obj_path, &obj_bytes).unwrap_or_else(|e| {
87+ eprintln!("Error writing object file: {}", e);
88+ process::exit(1);
89+ });
90+
91+ // Find runtime
92+ let runtime_path = find_runtime();
93+
94+ // Link
95+ let output_path = source_path.with_extension("");
96+ if let Err(e) = linker::link(&obj_path, &runtime_path, &output_path) {
97+ eprintln!("Link error: {}", e);
98+ let _ = std::fs::remove_file(&obj_path);
99+ process::exit(1);
100+ }
101+
102+ // Clean up
103+ let _ = std::fs::remove_file(&obj_path);
104+
105+ eprintln!("Compiled: {}", output_path.display());
106+}
107+
108+fn find_runtime() -> PathBuf {
109+ // 1. Environment variable override
110+ if let Ok(path) = std::env::var("KNOT_RUNTIME_LIB") {
111+ let p = PathBuf::from(path);
112+ if p.exists() {
113+ return p;
114+ }
115+ }
116+
117+ // 2. Same directory as the compiler executable
118+ if let Ok(exe) = std::env::current_exe() {
119+ if let Some(exe_dir) = exe.parent() {
120+ let candidate = exe_dir.join("libknot_runtime.a");
121+ if candidate.exists() {
122+ return candidate;
123+ }
124+ }
125+ }
126+
127+ eprintln!("Error: cannot find libknot_runtime.a");
128+ eprintln!("Ensure knot-runtime is built (cargo build -p knot-runtime)");
129+ eprintln!("Or set KNOT_RUNTIME_LIB=/path/to/libknot_runtime.a");
130+ process::exit(1);
131+}
crates/knot-compiler/src/types.rsnew file+153
@@ -0,0 +1,153 @@
1+//! Minimal type resolution for schema generation.
2+//!
3+//! Resolves type aliases and computes schema descriptors that the
4+//! runtime uses to create SQLite tables and read/write rows.
5+
6+use knot::ast::*;
7+use std::collections::HashMap;
8+
9+#[derive(Debug, Clone)]
10+pub enum ResolvedType {
11+ Int,
12+ Float,
13+ Text,
14+ Bool,
15+ Unit,
16+ Record(Vec<(String, ResolvedType)>),
17+ Relation(Box<ResolvedType>),
18+ Function(Box<ResolvedType>, Box<ResolvedType>),
19+ Named(String),
20+}
21+
22+pub struct TypeEnv {
23+ pub aliases: HashMap<String, ResolvedType>,
24+ /// constructor_name -> Vec<(field_name, field_type)>
25+ pub constructors: HashMap<String, Vec<(String, ResolvedType)>>,
26+ /// source_name -> schema descriptor string ("col:type,col:type,...")
27+ pub source_schemas: HashMap<String, String>,
28+}
29+
30+impl TypeEnv {
31+ pub fn from_module(module: &Module) -> Self {
32+ let mut aliases = HashMap::new();
33+ let mut constructors = HashMap::new();
34+ let mut source_schemas = HashMap::new();
35+
36+ // First pass: collect type aliases and data types
37+ for decl in &module.decls {
38+ match &decl.node {
39+ DeclKind::TypeAlias { name, params, ty } => {
40+ if params.is_empty() {
41+ let resolved = resolve_type(ty, &aliases);
42+ aliases.insert(name.clone(), resolved);
43+ }
44+ }
45+ DeclKind::Data {
46+ name,
47+ constructors: ctors,
48+ ..
49+ } => {
50+ // For single-variant data types, treat as a record alias
51+ if ctors.len() == 1 {
52+ let ctor = &ctors[0];
53+ let fields: Vec<(String, ResolvedType)> = ctor
54+ .fields
55+ .iter()
56+ .map(|f| (f.name.clone(), resolve_type(&f.value, &aliases)))
57+ .collect();
58+ aliases.insert(name.clone(), ResolvedType::Record(fields.clone()));
59+ constructors.insert(ctor.name.clone(), fields);
60+ } else {
61+ // Multi-variant: register each constructor
62+ for ctor in ctors {
63+ let fields: Vec<(String, ResolvedType)> = ctor
64+ .fields
65+ .iter()
66+ .map(|f| (f.name.clone(), resolve_type(&f.value, &aliases)))
67+ .collect();
68+ constructors.insert(ctor.name.clone(), fields);
69+ }
70+ }
71+ }
72+ _ => {}
73+ }
74+ }
75+
76+ // Second pass: compute source schemas
77+ for decl in &module.decls {
78+ if let DeclKind::Source { name, ty, .. } = &decl.node {
79+ let schema = schema_for_source(ty, &aliases);
80+ source_schemas.insert(name.clone(), schema);
81+ }
82+ }
83+
84+ Self {
85+ aliases,
86+ constructors,
87+ source_schemas,
88+ }
89+ }
90+}
91+
92+fn resolve_type(ty: &Type, aliases: &HashMap<String, ResolvedType>) -> ResolvedType {
93+ match &ty.node {
94+ TypeKind::Named(name) => match name.as_str() {
95+ "Int" => ResolvedType::Int,
96+ "Float" => ResolvedType::Float,
97+ "Text" => ResolvedType::Text,
98+ "Bool" => ResolvedType::Bool,
99+ _ => aliases
100+ .get(name)
101+ .cloned()
102+ .unwrap_or(ResolvedType::Named(name.clone())),
103+ },
104+ TypeKind::Record { fields, .. } => {
105+ let resolved: Vec<(String, ResolvedType)> = fields
106+ .iter()
107+ .map(|f| (f.name.clone(), resolve_type(&f.value, aliases)))
108+ .collect();
109+ ResolvedType::Record(resolved)
110+ }
111+ TypeKind::Relation(inner) => {
112+ ResolvedType::Relation(Box::new(resolve_type(inner, aliases)))
113+ }
114+ TypeKind::Function { param, result } => ResolvedType::Function(
115+ Box::new(resolve_type(param, aliases)),
116+ Box::new(resolve_type(result, aliases)),
117+ ),
118+ TypeKind::Var(_) => ResolvedType::Named("unknown".into()),
119+ TypeKind::App { .. } => ResolvedType::Named("unknown".into()),
120+ TypeKind::Variant { .. } => ResolvedType::Named("unknown".into()),
121+ TypeKind::Effectful { ty, .. } => resolve_type(ty, aliases),
122+ }
123+}
124+
125+fn schema_for_source(ty: &Type, aliases: &HashMap<String, ResolvedType>) -> String {
126+ match &ty.node {
127+ TypeKind::Relation(inner) => {
128+ let resolved = resolve_type(inner, aliases);
129+ schema_descriptor(&resolved)
130+ }
131+ _ => String::new(),
132+ }
133+}
134+
135+fn schema_descriptor(ty: &ResolvedType) -> String {
136+ match ty {
137+ ResolvedType::Record(fields) => fields
138+ .iter()
139+ .map(|(name, ty)| {
140+ let col_type = match ty {
141+ ResolvedType::Int => "int",
142+ ResolvedType::Float => "float",
143+ ResolvedType::Text => "text",
144+ ResolvedType::Bool => "bool",
145+ _ => "text",
146+ };
147+ format!("{}:{}", name, col_type)
148+ })
149+ .collect::<Vec<_>>()
150+ .join(","),
151+ _ => String::new(),
152+ }
153+}
crates/knot-runtime/Cargo.tomlnew file+10
@@ -0,0 +1,10 @@
1+[package]
2+name = "knot-runtime"
3+version.workspace = true
4+edition.workspace = true
5+
6+[lib]
7+crate-type = ["staticlib", "rlib"]
8+
9+[dependencies]
10+rusqlite = { workspace = true }
crates/knot-runtime/src/lib.rsnew file+882
@@ -0,0 +1,882 @@
1+//! Knot runtime library.
2+//!
3+//! Provides C-ABI functions for value management, relation operations,
4+//! and SQLite-backed persistence. This crate is compiled as a static
5+//! library and linked into every compiled Knot program.
6+
7+use rusqlite::Connection;
8+use std::ffi::c_void;
9+use std::slice;
10+
11+// ── Value representation ──────────────────────────────────────────
12+
13+/// Runtime representation of all Knot values.
14+///
15+/// Every Knot expression evaluates to a heap-allocated `Value`.
16+/// The Cranelift-generated code works exclusively with `*mut Value` pointers.
17+pub enum Value {
18+ Int(i64),
19+ Float(f64),
20+ Text(String),
21+ Bool(bool),
22+ Unit,
23+ Record(Vec<RecordField>),
24+ Relation(Vec<*mut Value>),
25+ Constructor(String, *mut Value),
26+ /// (fn_ptr, env) — fn_ptr has signature: extern "C" fn(db, env, arg) -> *mut Value
27+ Function(*const u8, *mut Value),
28+}
29+
30+pub struct RecordField {
31+ pub name: String,
32+ pub value: *mut Value,
33+}
34+
35+/// SQLite database handle.
36+pub struct KnotDb {
37+ pub conn: Connection,
38+}
39+
40+// ── Helpers ───────────────────────────────────────────────────────
41+
42+fn alloc(v: Value) -> *mut Value {
43+ Box::into_raw(Box::new(v))
44+}
45+
46+unsafe fn as_ref<'a>(v: *mut Value) -> &'a Value {
47+ unsafe { &*v }
48+}
49+
50+unsafe fn str_from_raw(ptr: *const u8, len: usize) -> &'static str {
51+ unsafe { std::str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) }
52+}
53+
54+// ── Value constructors ────────────────────────────────────────────
55+
56+#[unsafe(no_mangle)]
57+pub extern "C" fn knot_value_int(n: i64) -> *mut Value {
58+ alloc(Value::Int(n))
59+}
60+
61+#[unsafe(no_mangle)]
62+pub extern "C" fn knot_value_float(n: f64) -> *mut Value {
63+ alloc(Value::Float(n))
64+}
65+
66+#[unsafe(no_mangle)]
67+pub extern "C" fn knot_value_text(ptr: *const u8, len: usize) -> *mut Value {
68+ let s = unsafe { str_from_raw(ptr, len) };
69+ alloc(Value::Text(s.to_string()))
70+}
71+
72+#[unsafe(no_mangle)]
73+pub extern "C" fn knot_value_bool(b: i32) -> *mut Value {
74+ alloc(Value::Bool(b != 0))
75+}
76+
77+#[unsafe(no_mangle)]
78+pub extern "C" fn knot_value_unit() -> *mut Value {
79+ alloc(Value::Unit)
80+}
81+
82+#[unsafe(no_mangle)]
83+pub extern "C" fn knot_value_function(fn_ptr: *const u8, env: *mut Value) -> *mut Value {
84+ alloc(Value::Function(fn_ptr, env))
85+}
86+
87+#[unsafe(no_mangle)]
88+pub extern "C" fn knot_value_constructor(
89+ tag_ptr: *const u8,
90+ tag_len: usize,
91+ payload: *mut Value,
92+) -> *mut Value {
93+ let tag = unsafe { str_from_raw(tag_ptr, tag_len) }.to_string();
94+ alloc(Value::Constructor(tag, payload))
95+}
96+
97+// ── Value accessors ───────────────────────────────────────────────
98+
99+#[unsafe(no_mangle)]
100+pub extern "C" fn knot_value_get_int(v: *mut Value) -> i64 {
101+ match unsafe { as_ref(v) } {
102+ Value::Int(n) => *n,
103+ _ => panic!("knot runtime: expected Int"),
104+ }
105+}
106+
107+#[unsafe(no_mangle)]
108+pub extern "C" fn knot_value_get_float(v: *mut Value) -> f64 {
109+ match unsafe { as_ref(v) } {
110+ Value::Float(n) => *n,
111+ _ => panic!("knot runtime: expected Float"),
112+ }
113+}
114+
115+#[unsafe(no_mangle)]
116+pub extern "C" fn knot_value_get_bool(v: *mut Value) -> i32 {
117+ match unsafe { as_ref(v) } {
118+ Value::Bool(b) => *b as i32,
119+ _ => panic!("knot runtime: expected Bool"),
120+ }
121+}
122+
123+#[unsafe(no_mangle)]
124+pub extern "C" fn knot_value_get_tag(v: *mut Value) -> i32 {
125+ match unsafe { as_ref(v) } {
126+ Value::Int(_) => 0,
127+ Value::Float(_) => 1,
128+ Value::Text(_) => 2,
129+ Value::Bool(_) => 3,
130+ Value::Unit => 4,
131+ Value::Record(_) => 5,
132+ Value::Relation(_) => 6,
133+ Value::Constructor(_, _) => 7,
134+ Value::Function(_, _) => 8,
135+ }
136+}
137+
138+// ── Record operations ─────────────────────────────────────────────
139+
140+#[unsafe(no_mangle)]
141+pub extern "C" fn knot_record_empty(capacity: usize) -> *mut Value {
142+ alloc(Value::Record(Vec::with_capacity(capacity)))
143+}
144+
145+#[unsafe(no_mangle)]
146+pub extern "C" fn knot_record_set_field(
147+ record: *mut Value,
148+ key_ptr: *const u8,
149+ key_len: usize,
150+ value: *mut Value,
151+) {
152+ let name = unsafe { str_from_raw(key_ptr, key_len) }.to_string();
153+ let rec = unsafe { &mut *record };
154+ match rec {
155+ Value::Record(fields) => {
156+ // Update existing field or add new one
157+ if let Some(field) = fields.iter_mut().find(|f| f.name == name) {
158+ field.value = value;
159+ } else {
160+ fields.push(RecordField { name, value });
161+ }
162+ }
163+ _ => panic!("knot runtime: expected Record in set_field"),
164+ }
165+}
166+
167+#[unsafe(no_mangle)]
168+pub extern "C" fn knot_record_field(
169+ record: *mut Value,
170+ key_ptr: *const u8,
171+ key_len: usize,
172+) -> *mut Value {
173+ let name = unsafe { str_from_raw(key_ptr, key_len) };
174+ match unsafe { as_ref(record) } {
175+ Value::Record(fields) => {
176+ for field in fields {
177+ if field.name == name {
178+ return field.value;
179+ }
180+ }
181+ panic!("knot runtime: field '{}' not found", name)
182+ }
183+ Value::Constructor(_, payload) => {
184+ // Delegate to the payload (which should be a record)
185+ knot_record_field(*payload, key_ptr, key_len)
186+ }
187+ _ => panic!("knot runtime: expected Record in field access"),
188+ }
189+}
190+
191+// ── Relation operations ───────────────────────────────────────────
192+
193+#[unsafe(no_mangle)]
194+pub extern "C" fn knot_relation_empty() -> *mut Value {
195+ alloc(Value::Relation(Vec::new()))
196+}
197+
198+#[unsafe(no_mangle)]
199+pub extern "C" fn knot_relation_singleton(v: *mut Value) -> *mut Value {
200+ alloc(Value::Relation(vec![v]))
201+}
202+
203+#[unsafe(no_mangle)]
204+pub extern "C" fn knot_relation_push(rel: *mut Value, row: *mut Value) {
205+ let r = unsafe { &mut *rel };
206+ match r {
207+ Value::Relation(rows) => rows.push(row),
208+ _ => panic!("knot runtime: expected Relation in push"),
209+ }
210+}
211+
212+#[unsafe(no_mangle)]
213+pub extern "C" fn knot_relation_len(rel: *mut Value) -> usize {
214+ match unsafe { as_ref(rel) } {
215+ Value::Relation(rows) => rows.len(),
216+ _ => panic!("knot runtime: expected Relation in len"),
217+ }
218+}
219+
220+#[unsafe(no_mangle)]
221+pub extern "C" fn knot_relation_get(rel: *mut Value, index: usize) -> *mut Value {
222+ match unsafe { as_ref(rel) } {
223+ Value::Relation(rows) => rows[index],
224+ _ => panic!("knot runtime: expected Relation in get"),
225+ }
226+}
227+
228+#[unsafe(no_mangle)]
229+pub extern "C" fn knot_relation_union(a: *mut Value, b: *mut Value) -> *mut Value {
230+ let rows_a = match unsafe { as_ref(a) } {
231+ Value::Relation(rows) => rows.clone(),
232+ _ => panic!("knot runtime: expected Relation in union"),
233+ };
234+ let rows_b = match unsafe { as_ref(b) } {
235+ Value::Relation(rows) => rows.clone(),
236+ _ => panic!("knot runtime: expected Relation in union"),
237+ };
238+ let mut result = rows_a;
239+ for row in rows_b {
240+ if !result.iter().any(|existing| values_equal(*existing, row)) {
241+ result.push(row);
242+ }
243+ }
244+ alloc(Value::Relation(result))
245+}
246+
247+// ── Value equality ────────────────────────────────────────────────
248+
249+fn values_equal(a: *mut Value, b: *mut Value) -> bool {
250+ if a == b {
251+ return true;
252+ }
253+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
254+ (Value::Int(x), Value::Int(y)) => x == y,
255+ (Value::Float(x), Value::Float(y)) => x == y,
256+ (Value::Text(x), Value::Text(y)) => x == y,
257+ (Value::Bool(x), Value::Bool(y)) => x == y,
258+ (Value::Unit, Value::Unit) => true,
259+ (Value::Record(fa), Value::Record(fb)) => {
260+ if fa.len() != fb.len() {
261+ return false;
262+ }
263+ fa.iter().all(|field_a| {
264+ fb.iter()
265+ .any(|field_b| field_a.name == field_b.name && values_equal(field_a.value, field_b.value))
266+ })
267+ }
268+ (Value::Constructor(ta, pa), Value::Constructor(tb, pb)) => {
269+ ta == tb && values_equal(*pa, *pb)
270+ }
271+ (Value::Relation(ra), Value::Relation(rb)) => {
272+ if ra.len() != rb.len() {
273+ return false;
274+ }
275+ ra.iter()
276+ .all(|row_a| rb.iter().any(|row_b| values_equal(*row_a, *row_b)))
277+ && rb
278+ .iter()
279+ .all(|row_b| ra.iter().any(|row_a| values_equal(*row_a, *row_b)))
280+ }
281+ _ => false,
282+ }
283+}
284+
285+// ── Binary operations ─────────────────────────────────────────────
286+
287+#[unsafe(no_mangle)]
288+pub extern "C" fn knot_value_add(a: *mut Value, b: *mut Value) -> *mut Value {
289+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
290+ (Value::Int(x), Value::Int(y)) => alloc(Value::Int(x + y)),
291+ (Value::Float(x), Value::Float(y)) => alloc(Value::Float(x + y)),
292+ (Value::Int(x), Value::Float(y)) => alloc(Value::Float(*x as f64 + y)),
293+ (Value::Float(x), Value::Int(y)) => alloc(Value::Float(x + *y as f64)),
294+ _ => panic!("knot runtime: cannot add non-numeric values"),
295+ }
296+}
297+
298+#[unsafe(no_mangle)]
299+pub extern "C" fn knot_value_sub(a: *mut Value, b: *mut Value) -> *mut Value {
300+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
301+ (Value::Int(x), Value::Int(y)) => alloc(Value::Int(x - y)),
302+ (Value::Float(x), Value::Float(y)) => alloc(Value::Float(x - y)),
303+ (Value::Int(x), Value::Float(y)) => alloc(Value::Float(*x as f64 - y)),
304+ (Value::Float(x), Value::Int(y)) => alloc(Value::Float(x - *y as f64)),
305+ _ => panic!("knot runtime: cannot subtract non-numeric values"),
306+ }
307+}
308+
309+#[unsafe(no_mangle)]
310+pub extern "C" fn knot_value_mul(a: *mut Value, b: *mut Value) -> *mut Value {
311+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
312+ (Value::Int(x), Value::Int(y)) => alloc(Value::Int(x * y)),
313+ (Value::Float(x), Value::Float(y)) => alloc(Value::Float(x * y)),
314+ (Value::Int(x), Value::Float(y)) => alloc(Value::Float(*x as f64 * y)),
315+ (Value::Float(x), Value::Int(y)) => alloc(Value::Float(x * *y as f64)),
316+ _ => panic!("knot runtime: cannot multiply non-numeric values"),
317+ }
318+}
319+
320+#[unsafe(no_mangle)]
321+pub extern "C" fn knot_value_div(a: *mut Value, b: *mut Value) -> *mut Value {
322+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
323+ (Value::Int(x), Value::Int(y)) => {
324+ if *y == 0 {
325+ panic!("knot runtime: division by zero");
326+ }
327+ alloc(Value::Int(x / y))
328+ }
329+ (Value::Float(x), Value::Float(y)) => alloc(Value::Float(x / y)),
330+ (Value::Int(x), Value::Float(y)) => alloc(Value::Float(*x as f64 / y)),
331+ (Value::Float(x), Value::Int(y)) => alloc(Value::Float(x / *y as f64)),
332+ _ => panic!("knot runtime: cannot divide non-numeric values"),
333+ }
334+}
335+
336+#[unsafe(no_mangle)]
337+pub extern "C" fn knot_value_eq(a: *mut Value, b: *mut Value) -> *mut Value {
338+ alloc(Value::Bool(values_equal(a, b)))
339+}
340+
341+#[unsafe(no_mangle)]
342+pub extern "C" fn knot_value_neq(a: *mut Value, b: *mut Value) -> *mut Value {
343+ alloc(Value::Bool(!values_equal(a, b)))
344+}
345+
346+#[unsafe(no_mangle)]
347+pub extern "C" fn knot_value_lt(a: *mut Value, b: *mut Value) -> *mut Value {
348+ let result = match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
349+ (Value::Int(x), Value::Int(y)) => x < y,
350+ (Value::Float(x), Value::Float(y)) => x < y,
351+ (Value::Int(x), Value::Float(y)) => (*x as f64) < *y,
352+ (Value::Float(x), Value::Int(y)) => *x < (*y as f64),
353+ (Value::Text(x), Value::Text(y)) => x < y,
354+ _ => panic!("knot runtime: cannot compare values with <"),
355+ };
356+ alloc(Value::Bool(result))
357+}
358+
359+#[unsafe(no_mangle)]
360+pub extern "C" fn knot_value_gt(a: *mut Value, b: *mut Value) -> *mut Value {
361+ let result = match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
362+ (Value::Int(x), Value::Int(y)) => x > y,
363+ (Value::Float(x), Value::Float(y)) => x > y,
364+ (Value::Int(x), Value::Float(y)) => (*x as f64) > *y,
365+ (Value::Float(x), Value::Int(y)) => *x > (*y as f64),
366+ (Value::Text(x), Value::Text(y)) => x > y,
367+ _ => panic!("knot runtime: cannot compare values with >"),
368+ };
369+ alloc(Value::Bool(result))
370+}
371+
372+#[unsafe(no_mangle)]
373+pub extern "C" fn knot_value_le(a: *mut Value, b: *mut Value) -> *mut Value {
374+ let result = match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
375+ (Value::Int(x), Value::Int(y)) => x <= y,
376+ (Value::Float(x), Value::Float(y)) => x <= y,
377+ (Value::Int(x), Value::Float(y)) => (*x as f64) <= *y,
378+ (Value::Float(x), Value::Int(y)) => *x <= (*y as f64),
379+ (Value::Text(x), Value::Text(y)) => x <= y,
380+ _ => panic!("knot runtime: cannot compare values with <="),
381+ };
382+ alloc(Value::Bool(result))
383+}
384+
385+#[unsafe(no_mangle)]
386+pub extern "C" fn knot_value_ge(a: *mut Value, b: *mut Value) -> *mut Value {
387+ let result = match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
388+ (Value::Int(x), Value::Int(y)) => x >= y,
389+ (Value::Float(x), Value::Float(y)) => x >= y,
390+ (Value::Int(x), Value::Float(y)) => (*x as f64) >= *y,
391+ (Value::Float(x), Value::Int(y)) => *x >= (*y as f64),
392+ (Value::Text(x), Value::Text(y)) => x >= y,
393+ _ => panic!("knot runtime: cannot compare values with >="),
394+ };
395+ alloc(Value::Bool(result))
396+}
397+
398+#[unsafe(no_mangle)]
399+pub extern "C" fn knot_value_and(a: *mut Value, b: *mut Value) -> *mut Value {
400+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
401+ (Value::Bool(x), Value::Bool(y)) => alloc(Value::Bool(*x && *y)),
402+ _ => panic!("knot runtime: && requires Bool operands"),
403+ }
404+}
405+
406+#[unsafe(no_mangle)]
407+pub extern "C" fn knot_value_or(a: *mut Value, b: *mut Value) -> *mut Value {
408+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
409+ (Value::Bool(x), Value::Bool(y)) => alloc(Value::Bool(*x || *y)),
410+ _ => panic!("knot runtime: || requires Bool operands"),
411+ }
412+}
413+
414+#[unsafe(no_mangle)]
415+pub extern "C" fn knot_value_concat(a: *mut Value, b: *mut Value) -> *mut Value {
416+ match (unsafe { as_ref(a) }, unsafe { as_ref(b) }) {
417+ (Value::Text(x), Value::Text(y)) => {
418+ let mut s = x.clone();
419+ s.push_str(y);
420+ alloc(Value::Text(s))
421+ }
422+ (Value::Relation(_x), Value::Relation(_y)) => {
423+ // ++ on relations is union
424+ knot_relation_union(a, b)
425+ }
426+ _ => panic!("knot runtime: ++ requires Text or Relation operands"),
427+ }
428+}
429+
430+// ── Unary operations ──────────────────────────────────────────────
431+
432+#[unsafe(no_mangle)]
433+pub extern "C" fn knot_value_negate(v: *mut Value) -> *mut Value {
434+ match unsafe { as_ref(v) } {
435+ Value::Int(n) => alloc(Value::Int(-n)),
436+ Value::Float(n) => alloc(Value::Float(-n)),
437+ _ => panic!("knot runtime: cannot negate non-numeric value"),
438+ }
439+}
440+
441+#[unsafe(no_mangle)]
442+pub extern "C" fn knot_value_not(v: *mut Value) -> *mut Value {
443+ match unsafe { as_ref(v) } {
444+ Value::Bool(b) => alloc(Value::Bool(!b)),
445+ _ => panic!("knot runtime: 'not' requires Bool operand"),
446+ }
447+}
448+
449+// ── Function calls ────────────────────────────────────────────────
450+
451+/// Call a function value: fn_ptr(db, env, arg) -> result
452+#[unsafe(no_mangle)]
453+pub extern "C" fn knot_value_call(
454+ db: *mut c_void,
455+ func: *mut Value,
456+ arg: *mut Value,
457+) -> *mut Value {
458+ match unsafe { as_ref(func) } {
459+ Value::Function(fn_ptr, env) => {
460+ let f: extern "C" fn(*mut c_void, *mut Value, *mut Value) -> *mut Value =
461+ unsafe { std::mem::transmute(*fn_ptr) };
462+ f(db, *env, arg)
463+ }
464+ _ => panic!("knot runtime: cannot call non-function value"),
465+ }
466+}
467+
468+// ── Printing ──────────────────────────────────────────────────────
469+
470+fn format_value(v: *mut Value) -> String {
471+ if v.is_null() {
472+ return "null".to_string();
473+ }
474+ match unsafe { as_ref(v) } {
475+ Value::Int(n) => n.to_string(),
476+ Value::Float(n) => {
477+ if *n == (*n as i64) as f64 {
478+ format!("{:.1}", n)
479+ } else {
480+ n.to_string()
481+ }
482+ }
483+ Value::Text(s) => format!("\"{}\"", s),
484+ Value::Bool(b) => {
485+ if *b {
486+ "True {}".to_string()
487+ } else {
488+ "False {}".to_string()
489+ }
490+ }
491+ Value::Unit => "{}".to_string(),
492+ Value::Record(fields) => {
493+ let inner: Vec<String> = fields
494+ .iter()
495+ .map(|f| format!("{}: {}", f.name, format_value(f.value)))
496+ .collect();
497+ format!("{{{}}}", inner.join(", "))
498+ }
499+ Value::Relation(rows) => {
500+ let inner: Vec<String> = rows.iter().map(|r| format_value(*r)).collect();
501+ format!("[{}]", inner.join(", "))
502+ }
503+ Value::Constructor(tag, payload) => {
504+ let p = format_value(*payload);
505+ if p == "{}" {
506+ format!("{} {{}}", tag)
507+ } else {
508+ format!("{} {}", tag, p)
509+ }
510+ }
511+ Value::Function(_, _) => "<function>".to_string(),
512+ }
513+}
514+
515+#[unsafe(no_mangle)]
516+pub extern "C" fn knot_print(v: *mut Value) -> *mut Value {
517+ print!("{}", format_value(v));
518+ alloc(Value::Unit)
519+}
520+
521+#[unsafe(no_mangle)]
522+pub extern "C" fn knot_println(v: *mut Value) -> *mut Value {
523+ println!("{}", format_value(v));
524+ alloc(Value::Unit)
525+}
526+
527+// ── Database operations ───────────────────────────────────────────
528+
529+#[unsafe(no_mangle)]
530+pub extern "C" fn knot_db_open(path_ptr: *const u8, path_len: usize) -> *mut c_void {
531+ let path = unsafe { str_from_raw(path_ptr, path_len) };
532+ let conn = Connection::open(path).expect("knot runtime: failed to open database");
533+ conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
534+ .expect("knot runtime: failed to set pragmas");
535+ let db = Box::new(KnotDb { conn });
536+ Box::into_raw(db) as *mut c_void
537+}
538+
539+#[unsafe(no_mangle)]
540+pub extern "C" fn knot_db_close(db: *mut c_void) {
541+ if !db.is_null() {
542+ let _ = unsafe { Box::from_raw(db as *mut KnotDb) };
543+ }
544+}
545+
546+/// Execute a SQL statement (e.g., CREATE TABLE).
547+#[unsafe(no_mangle)]
548+pub extern "C" fn knot_db_exec(db: *mut c_void, sql_ptr: *const u8, sql_len: usize) {
549+ let db = unsafe { &*(db as *mut KnotDb) };
550+ let sql = unsafe { str_from_raw(sql_ptr, sql_len) };
551+ db.conn
552+ .execute_batch(sql)
553+ .unwrap_or_else(|e| panic!("knot runtime: SQL error: {}\n SQL: {}", e, sql));
554+}
555+
556+// ── Source operations ─────────────────────────────────────────────
557+
558+/// Schema descriptor format: "col1:type1,col2:type2,..."
559+/// Types: "int", "float", "text", "bool"
560+struct ColumnSpec {
561+ name: String,
562+ ty: ColType,
563+}
564+
565+#[derive(Clone, Copy)]
566+enum ColType {
567+ Int,
568+ Float,
569+ Text,
570+ Bool,
571+}
572+
573+fn parse_schema(spec: &str) -> Vec<ColumnSpec> {
574+ if spec.is_empty() {
575+ return Vec::new();
576+ }
577+ spec.split(',')
578+ .map(|part| {
579+ let mut parts = part.splitn(2, ':');
580+ let name = parts.next().unwrap().to_string();
581+ let ty = match parts.next().unwrap_or("text") {
582+ "int" => ColType::Int,
583+ "float" => ColType::Float,
584+ "text" => ColType::Text,
585+ "bool" => ColType::Bool,
586+ other => panic!("knot runtime: unknown column type '{}'", other),
587+ };
588+ ColumnSpec { name, ty }
589+ })
590+ .collect()
591+}
592+
593+fn sql_type(ty: ColType) -> &'static str {
594+ match ty {
595+ ColType::Int => "INTEGER",
596+ ColType::Float => "REAL",
597+ ColType::Text => "TEXT",
598+ ColType::Bool => "INTEGER",
599+ }
600+}
601+
602+/// Initialize a source table. Creates it if it doesn't exist.
603+#[unsafe(no_mangle)]
604+pub extern "C" fn knot_source_init(
605+ db: *mut c_void,
606+ name_ptr: *const u8,
607+ name_len: usize,
608+ schema_ptr: *const u8,
609+ schema_len: usize,
610+) {
611+ let db_ref = unsafe { &*(db as *mut KnotDb) };
612+ let name = unsafe { str_from_raw(name_ptr, name_len) };
613+ let schema = unsafe { str_from_raw(schema_ptr, schema_len) };
614+ let cols = parse_schema(schema);
615+
616+ let col_defs: Vec<String> = cols
617+ .iter()
618+ .map(|c| format!("\"{}\" {}", c.name, sql_type(c.ty)))
619+ .collect();
620+ let col_names: Vec<String> = cols.iter().map(|c| format!("\"{}\"", c.name)).collect();
621+
622+ let sql = format!(
623+ "CREATE TABLE IF NOT EXISTS \"_knot_{}\" ({});",
624+ name,
625+ col_defs.join(", ")
626+ );
627+ db_ref.conn.execute_batch(&sql).unwrap_or_else(|e| {
628+ panic!("knot runtime: failed to create table '{}': {}", name, e)
629+ });
630+
631+ // Create unique index for set semantics (ignore if already exists)
632+ if !cols.is_empty() {
633+ let idx_sql = format!(
634+ "CREATE UNIQUE INDEX IF NOT EXISTS \"_knot_{}_unique\" ON \"_knot_{}\" ({});",
635+ name,
636+ name,
637+ col_names.join(", ")
638+ );
639+ let _ = db_ref.conn.execute_batch(&idx_sql);
640+ }
641+}
642+
643+/// Read all rows from a source relation.
644+#[unsafe(no_mangle)]
645+pub extern "C" fn knot_source_read(
646+ db: *mut c_void,
647+ name_ptr: *const u8,
648+ name_len: usize,
649+ schema_ptr: *const u8,
650+ schema_len: usize,
651+) -> *mut Value {
652+ let db_ref = unsafe { &*(db as *mut KnotDb) };
653+ let name = unsafe { str_from_raw(name_ptr, name_len) };
654+ let schema = unsafe { str_from_raw(schema_ptr, schema_len) };
655+ let cols = parse_schema(schema);
656+
657+ let col_names: Vec<String> = cols.iter().map(|c| format!("\"{}\"", c.name)).collect();
658+ let sql = format!(
659+ "SELECT {} FROM \"_knot_{}\"",
660+ if col_names.is_empty() {
661+ "1".to_string()
662+ } else {
663+ col_names.join(", ")
664+ },
665+ name
666+ );
667+
668+ let mut stmt = db_ref
669+ .conn
670+ .prepare(&sql)
671+ .unwrap_or_else(|e| panic!("knot runtime: query error: {}", e));
672+
673+ let mut rows: Vec<*mut Value> = Vec::new();
674+ let mut result_rows = stmt
675+ .query([])
676+ .unwrap_or_else(|e| panic!("knot runtime: query exec error: {}", e));
677+
678+ while let Some(row) = result_rows
679+ .next()
680+ .unwrap_or_else(|e| panic!("knot runtime: row fetch error: {}", e))
681+ {
682+ let record = knot_record_empty(cols.len());
683+ for (i, col) in cols.iter().enumerate() {
684+ let val = match col.ty {
685+ ColType::Int => knot_value_int(row.get::<_, i64>(i).unwrap()),
686+ ColType::Float => knot_value_float(row.get::<_, f64>(i).unwrap()),
687+ ColType::Text => {
688+ let s: String = row.get(i).unwrap();
689+ let v = alloc(Value::Text(s));
690+ v
691+ }
692+ ColType::Bool => knot_value_bool(row.get::<_, i32>(i).unwrap()),
693+ };
694+ // Set the field
695+ let name = col.name.as_bytes();
696+ knot_record_set_field(record, name.as_ptr(), name.len(), val);
697+ }
698+ rows.push(record);
699+ }
700+
701+ alloc(Value::Relation(rows))
702+}
703+
704+/// Write a relation to a source (replaces all rows).
705+#[unsafe(no_mangle)]
706+pub extern "C" fn knot_source_write(
707+ db: *mut c_void,
708+ name_ptr: *const u8,
709+ name_len: usize,
710+ schema_ptr: *const u8,
711+ schema_len: usize,
712+ relation: *mut Value,
713+) {
714+ let db_ref = unsafe { &*(db as *mut KnotDb) };
715+ let name = unsafe { str_from_raw(name_ptr, name_len) };
716+ let schema = unsafe { str_from_raw(schema_ptr, schema_len) };
717+ let cols = parse_schema(schema);
718+
719+ let rows = match unsafe { as_ref(relation) } {
720+ Value::Relation(rows) => rows,
721+ _ => panic!("knot runtime: source_write expects a Relation"),
722+ };
723+
724+ // Delete all existing rows and insert new ones in a transaction
725+ db_ref
726+ .conn
727+ .execute_batch("BEGIN;")
728+ .expect("knot runtime: failed to begin transaction");
729+
730+ let delete_sql = format!("DELETE FROM \"_knot_{}\";", name);
731+ db_ref
732+ .conn
733+ .execute_batch(&delete_sql)
734+ .expect("knot runtime: failed to delete rows");
735+
736+ if !cols.is_empty() && !rows.is_empty() {
737+ let col_names: Vec<String> = cols.iter().map(|c| format!("\"{}\"", c.name)).collect();
738+ let placeholders: Vec<String> = cols.iter().enumerate().map(|(i, _)| format!("?{}", i + 1)).collect();
739+ let insert_sql = format!(
740+ "INSERT OR IGNORE INTO \"_knot_{}\" ({}) VALUES ({});",
741+ name,
742+ col_names.join(", "),
743+ placeholders.join(", ")
744+ );
745+
746+ let mut stmt = db_ref
747+ .conn
748+ .prepare(&insert_sql)
749+ .expect("knot runtime: failed to prepare insert");
750+
751+ for row_ptr in rows {
752+ let row = unsafe { as_ref(*row_ptr) };
753+ match row {
754+ Value::Record(fields) => {
755+ let params: Vec<rusqlite::types::Value> = cols
756+ .iter()
757+ .map(|col| {
758+ let field = fields
759+ .iter()
760+ .find(|f| f.name == col.name)
761+ .unwrap_or_else(|| {
762+ panic!(
763+ "knot runtime: missing field '{}' in record",
764+ col.name
765+ )
766+ });
767+ value_to_sqlite(field.value, col.ty)
768+ })
769+ .collect();
770+ let param_refs: Vec<&dyn rusqlite::types::ToSql> =
771+ params.iter().map(|p| p as &dyn rusqlite::types::ToSql).collect();
772+ stmt.execute(param_refs.as_slice()).unwrap_or_else(|e| {
773+ panic!("knot runtime: insert error: {}", e)
774+ });
775+ }
776+ _ => panic!("knot runtime: relation rows must be records"),
777+ }
778+ }
779+ }
780+
781+ db_ref
782+ .conn
783+ .execute_batch("COMMIT;")
784+ .expect("knot runtime: failed to commit transaction");
785+}
786+
787+fn value_to_sqlite(v: *mut Value, ty: ColType) -> rusqlite::types::Value {
788+ match (unsafe { as_ref(v) }, ty) {
789+ (Value::Int(n), _) => rusqlite::types::Value::Integer(*n),
790+ (Value::Float(n), _) => rusqlite::types::Value::Real(*n),
791+ (Value::Text(s), _) => rusqlite::types::Value::Text(s.clone()),
792+ (Value::Bool(b), _) => rusqlite::types::Value::Integer(*b as i64),
793+ _ => panic!("knot runtime: cannot convert value to SQL"),
794+ }
795+}
796+
797+// ── Atomic (transactions) ─────────────────────────────────────────
798+
799+#[unsafe(no_mangle)]
800+pub extern "C" fn knot_atomic_begin(db: *mut c_void) {
801+ let db_ref = unsafe { &*(db as *mut KnotDb) };
802+ db_ref
803+ .conn
804+ .execute_batch("SAVEPOINT knot_atomic;")
805+ .expect("knot runtime: failed to begin atomic");
806+}
807+
808+#[unsafe(no_mangle)]
809+pub extern "C" fn knot_atomic_commit(db: *mut c_void) {
810+ let db_ref = unsafe { &*(db as *mut KnotDb) };
811+ db_ref
812+ .conn
813+ .execute_batch("RELEASE SAVEPOINT knot_atomic;")
814+ .expect("knot runtime: failed to commit atomic");
815+}
816+
817+#[unsafe(no_mangle)]
818+pub extern "C" fn knot_atomic_rollback(db: *mut c_void) {
819+ let db_ref = unsafe { &*(db as *mut KnotDb) };
820+ db_ref
821+ .conn
822+ .execute_batch("ROLLBACK TO SAVEPOINT knot_atomic;")
823+ .expect("knot runtime: failed to rollback atomic");
824+}
825+
826+// ── Record update ─────────────────────────────────────────────────
827+
828+/// Create a new record by copying `base` and overriding fields.
829+/// This implements `{base | field1: val1, field2: val2}`.
830+#[unsafe(no_mangle)]
831+pub extern "C" fn knot_record_update(base: *mut Value) -> *mut Value {
832+ match unsafe { as_ref(base) } {
833+ Value::Record(fields) => {
834+ let new_fields: Vec<RecordField> = fields
835+ .iter()
836+ .map(|f| RecordField {
837+ name: f.name.clone(),
838+ value: f.value,
839+ })
840+ .collect();
841+ alloc(Value::Record(new_fields))
842+ }
843+ _ => panic!("knot runtime: record update requires a Record base"),
844+ }
845+}
846+
847+// ── Pipe (|>) support ─────────────────────────────────────────────
848+
849+/// Apply a function value to an argument: `arg |> func`
850+#[unsafe(no_mangle)]
851+pub extern "C" fn knot_value_pipe(
852+ db: *mut c_void,
853+ arg: *mut Value,
854+ func: *mut Value,
855+) -> *mut Value {
856+ knot_value_call(db, func, arg)
857+}
858+
859+// ── Constructor matching ──────────────────────────────────────────
860+
861+/// Check if a value is a constructor with the given tag.
862+#[unsafe(no_mangle)]
863+pub extern "C" fn knot_constructor_matches(
864+ v: *mut Value,
865+ tag_ptr: *const u8,
866+ tag_len: usize,
867+) -> i32 {
868+ let tag = unsafe { str_from_raw(tag_ptr, tag_len) };
869+ match unsafe { as_ref(v) } {
870+ Value::Constructor(t, _) => (t == tag) as i32,
871+ _ => 0,
872+ }
873+}
874+
875+/// Get the payload of a constructor value.
876+#[unsafe(no_mangle)]
877+pub extern "C" fn knot_constructor_payload(v: *mut Value) -> *mut Value {
878+ match unsafe { as_ref(v) } {
879+ Value::Constructor(_, payload) => *payload,
880+ _ => panic!("knot runtime: expected Constructor"),
881+ }
882+}
crates/knot/Cargo.tomlnew file+4
@@ -0,0 +1,4 @@
1+[package]
2+name = "knot"
3+version.workspace = true
4+edition.workspace = true
src/ast.rs → crates/knot/src/ast.rsrenamed

The content is the same.

src/diagnostic.rs → crates/knot/src/diagnostic.rsrenamed

The content is the same.

src/lexer.rs → crates/knot/src/lexer.rsrenamed

The content is the same.

src/lib.rs → crates/knot/src/lib.rsrenamed

The content is the same.

src/parser.rs → crates/knot/src/parser.rsrenamed

The content is the same.

tests/parser.rs → crates/knot/tests/parser.rsrenamed

The content is the same.

examples/hello.knotnew file+9
@@ -0,0 +1,9 @@
1+type Person = {name: Text, age: Int}
2+
3+*people : [Person]
4+
5+main = do
6+ set *people = [{name: "Alice", age: 30}, {name: "Bob", age: 25}, {name: "Carol", age: 35}]
7+ p <- *people
8+ where p.age > 27
9+ yield p.name
examples/todo.knotnew file+26
@@ -0,0 +1,26 @@
1+type Todo = {title: Text, owner: Text, done: Int}
2+
3+*todos : [Todo]
4+
5+add title owner =
6+ set *todos = union *todos [{title: title, owner: owner, done: 0}]
7+
8+complete title =
9+ set *todos = do
10+ t <- *todos
11+ yield (if t.title == title
12+ then {t | done: 1}
13+ else t)
14+
15+pending owner = do
16+ t <- *todos
17+ where t.done == 0
18+ where t.owner == owner
19+ yield t.title
20+
21+main = do
22+ add "Write parser" "Alice"
23+ add "Write codegen" "Alice"
24+ add "Write runtime" "Bob"
25+ complete "Write parser"
26+ yield (pending "Alice")