@@ -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(¶ms[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, ¶m_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+}