on master help sign in
Scalar aggregates return a one-element relation (Rel a -> Rel <scalar>)

sum, avg, fold, countWhere, minOn, maxOn now yield a one-element relation, like count: they compose into further queries and the number is extracted with run. Each in-memory codegen path wraps the scalar in knot_relation_singleton; the SQL pushdown terminal (QueryTerminal::Aggregate) and the direct sum path do too. New define_stdlib_fn_2_rel / _3_rel helpers wrap the result; avg/minOn/maxOn/ countWhere/fold use them. Tests that used these as bare numbers now expect the one-element relation ([N]). Suite 290/0.

commitba31a8c2695dad89bf882af50613dd349f0d41dd authoriko <mail@iko.soy> dateSat 29 Aug 2026, 13:54 (1 month ago) parent2efe921
5 files changed+63−19
crates/knot-compiler/src/codegen.rs+47−9 crates/knot-compiler/src/infer.rs+12−6 crates/knot-jit/tests/numeric_text.rs+1−1 crates/knot-jit/tests/persistence.rs+2−2 crates/knot-jit/tests/relations.rs+1−1
crates/knot-compiler/src/codegen.rs+47−9
@@ -2198,7 +2198,9 @@ impl<M: cranelift_module::Module> Codegen<M> {
21982198 let db = builder.block_params(entry)[0];
21992199 let rel = builder.block_params(entry)[1];
22002200 let is_float = builder.ins().iconst(types::I64, 0);
2201− let result = cg.call_rt(builder, "knot_relation_sum_direct", &[db, rel, is_float]);
2201+ let n = cg.call_rt(builder, "knot_relation_sum_direct", &[db, rel, is_float]);
2202+ // sum : Rel a -> Rel a — a one-element relation.
2203+ let result = cg.call_rt(builder, "knot_relation_singleton", &[n]);
22022204 builder.ins().return_(&[result]);
22032205 });
22042206 }
@@ -2304,6 +2306,17 @@ impl<M: cranelift_module::Module> Codegen<M> {
23042306 /// outer(db, arg1) -> Function(inner, arg1, name) — arg1 passed directly as env
23052307 /// inner(db, env=arg1, arg2) -> rt_fn(db, arg1, arg2)
23062308 fn define_stdlib_fn_2(&mut self, name: &str, rt_name: &str, rt_needs_db: bool) {
2309+ self.define_stdlib_fn_2_wrap(name, rt_name, rt_needs_db, false);
2310+ }
2311+
2312+ /// Like `define_stdlib_fn_2`, but wraps the scalar result in a one-element
2313+ /// relation (for the scalar aggregates — sum/avg/minOn/maxOn/countWhere —
2314+ /// whose result is a `Rel <scalar>`).
2315+ fn define_stdlib_fn_2_rel(&mut self, name: &str, rt_name: &str, rt_needs_db: bool) {
2316+ self.define_stdlib_fn_2_wrap(name, rt_name, rt_needs_db, true);
2317+ }
2318+
2319+ fn define_stdlib_fn_2_wrap(&mut self, name: &str, rt_name: &str, rt_needs_db: bool, wrap_singleton: bool) {
23072320 // Stdlib fns are always under the flattened `base.<name>` key — no
23082321 // shadow branch; the bare name is the user's alone.
23092322 let inner_id = self.declare_closure_fn(&format!("__stdlib_{}_apply", name));
@@ -2349,6 +2362,11 @@ impl<M: cranelift_module::Module> Codegen<M> {
23492362 } else {
23502363 cg.call_rt(builder, &rt_name, &[arg1, arg2])
23512364 };
2365+ let result = if wrap_singleton {
2366+ cg.call_rt(builder, "knot_relation_singleton", &[result])
2367+ } else {
2368+ result
2369+ };
23522370 builder.ins().return_(&[result]);
23532371 });
23542372 }
@@ -2358,6 +2376,16 @@ impl<M: cranelift_module::Module> Codegen<M> {
23582376 /// middle(db, {arg1}, arg2) -> Function(inner, {arg1, arg2})
23592377 /// inner(db, {arg1, arg2}, arg3) -> rt_fn(db, arg1, arg2, arg3)
23602378 fn define_stdlib_fn_3(&mut self, name: &str, rt_name: &str) {
2379+ self.define_stdlib_fn_3_wrap(name, rt_name, false);
2380+ }
2381+
2382+ /// Like `define_stdlib_fn_3`, but wraps the scalar result in a one-element
2383+ /// relation (for `fold`, whose result is a `Rel b`).
2384+ fn define_stdlib_fn_3_rel(&mut self, name: &str, rt_name: &str) {
2385+ self.define_stdlib_fn_3_wrap(name, rt_name, true);
2386+ }
2387+
2388+ fn define_stdlib_fn_3_wrap(&mut self, name: &str, rt_name: &str, wrap_singleton: bool) {
23612389 // Stdlib fns are always under the flattened `base.<name>` key — no
23622390 // shadow branch; the bare name is the user's alone.
23632391 let middle_id = self.declare_closure_fn(&format!("__stdlib_{}_mid", name));
@@ -2450,6 +2478,11 @@ impl<M: cranelift_module::Module> Codegen<M> {
24502478 let arg2 = cg.call_rt(builder, "knot_record_field_by_index", &[env, idx1]);
24512479
24522480 let result = cg.call_rt(builder, &rt_name, &[db, arg1, arg2, arg3]);
2481+ let result = if wrap_singleton {
2482+ cg.call_rt(builder, "knot_relation_singleton", &[result])
2483+ } else {
2484+ result
2485+ };
24532486 builder.ins().return_(&[result]);
24542487 });
24552488 }
@@ -3405,7 +3438,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
34053438 self.define_stdlib_fn_2("map", "knot_relation_map", true);
34063439 self.define_stdlib_fn_2("forEach", "knot_relation_for_each", true);
34073440 self.define_stdlib_fn_2("findFirst", "knot_relation_find_first", true);
3408− self.define_stdlib_fn_3("fold", "knot_relation_fold");
3441+ self.define_stdlib_fn_3_rel("fold", "knot_relation_fold");
34093442 self.define_stdlib_fn_2("traverse", "knot_relation_traverse", true);
34103443 self.define_stdlib_fn_2("take", "knot_take", false);
34113444 self.define_stdlib_fn_2("drop", "knot_drop", false);
@@ -3426,10 +3459,10 @@ impl<M: cranelift_module::Module> Codegen<M> {
34263459 self.define_stdlib_fn_1("valueNegate", "knot_value_negate");
34273460 self.define_stdlib_fn_2("union", "knot_relation_union", true);
34283461 self.define_stdlib_fn_2("bind", "knot_relation_bind", true);
3429− self.define_stdlib_fn_2("avg", "knot_relation_avg", true);
3430− self.define_stdlib_fn_2("minOn", "knot_relation_min", true);
3431− self.define_stdlib_fn_2("maxOn", "knot_relation_max", true);
3432− self.define_stdlib_fn_2("countWhere", "knot_relation_count_where", true);
3462+ self.define_stdlib_fn_2_rel("avg", "knot_relation_avg", true);
3463+ self.define_stdlib_fn_2_rel("minOn", "knot_relation_min", true);
3464+ self.define_stdlib_fn_2_rel("maxOn", "knot_relation_max", true);
3465+ self.define_stdlib_fn_2_rel("countWhere", "knot_relation_count_where", true);
34333466 self.define_stdlib_fn_2("any", "knot_relation_any", true);
34343467 self.define_stdlib_fn_2("all", "knot_relation_all", true);
34353468
@@ -8914,11 +8947,13 @@ impl<M: cranelift_module::Module> Codegen<M> {
89148947 let is_float = builder
89158948 .ins()
89168949 .iconst(types::I64, self.sum_float_spans.contains(&expr.span) as i64);
8917− return self.call_rt(
8950+ let n = self.call_rt(
89188951 builder,
89198952 "knot_relation_sum_direct",
89208953 &[db, rel_val, is_float],
89218954 );
8955+ // sum : Rel a -> Rel a — a one-element relation.
8956+ return self.call_rt(builder, "knot_relation_singleton", &[n]);
89228957 }
89238958
89248959 // Implicit dictionary: prepend the record resolved during inference as
@@ -16871,12 +16906,15 @@ impl<M: cranelift_module::Module> Codegen<M> {
1687116906 let rt_fn = query.runtime_fn();
1687216907 // SUM and MIN/MAX take an extra I64 flag (is_float / is_text);
1687316908 // AVG (`knot_source_query_float`) takes none.
16874− if matches!(rt_fn, "knot_source_query_sum" | "knot_source_query_value") {
16909+ let n = if matches!(rt_fn, "knot_source_query_sum" | "knot_source_query_value") {
1687516910 let flag = builder.ins().iconst(types::I64, *result_flag as i64);
1687616911 self.call_rt(builder, rt_fn, &[db, sql_ptr, sql_len, params_rel, flag])
1687716912 } else {
1687816913 self.call_rt(builder, rt_fn, &[db, sql_ptr, sql_len, params_rel])
16879− }
16914+ };
16915+ // The scalar aggregates (sum/avg/minOn/maxOn) yield a one-element
16916+ // relation (Rel <scalar>), so they compose into further queries.
16917+ self.call_rt(builder, "knot_relation_singleton", &[n])
1688016918 }
1688116919 }
1688216920 }
crates/knot-compiler/src/infer.rs+12−6
@@ -10972,9 +10972,10 @@ impl Infer {
1097210972 unit_binops: vec![],
1097310973 ty: Ty::Fun(
1097410974 Box::new(Ty::Fun(Box::new(Ty::Var(a)), Box::new(Ty::Bool))),
10975+ // countWhere : ... -> Rel (Int u) — a one-element relation.
1097510976 Box::new(Ty::Fun(
1097610977 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
10977− Box::new(int_u),
10978+ Box::new(Ty::Relation(Box::new(int_u))),
1097810979 )),
1097910980 ),
1098010981 },
@@ -11317,9 +11318,10 @@ impl Infer {
1131711318 )),
1131811319 Box::new(Ty::Fun(
1131911320 Box::new(Ty::Var(b)),
11321+ // fold : ... -> Rel b — the folded value is a one-element relation.
1132011322 Box::new(Ty::Fun(
1132111323 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
11322− Box::new(Ty::Var(b)),
11324+ Box::new(Ty::Relation(Box::new(Ty::Var(b)))),
1132311325 )),
1132411326 )),
1132511327 ),
@@ -11482,9 +11484,10 @@ impl Infer {
1148211484 span: Span::new(0, 0),
1148311485 }],
1148411486 unit_binops: vec![],
11487+ // sum : Rel a -> Rel a — the sum is a one-element relation.
1148511488 ty: Ty::Fun(
1148611489 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
11487− Box::new(Ty::Var(a)),
11490+ Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
1148811491 ),
1148911492 },
1149011493 );
@@ -11503,9 +11506,10 @@ impl Infer {
1150311506 unit_binops: vec![],
1150411507 ty: Ty::Fun(
1150511508 Box::new(Ty::Fun(Box::new(Ty::Var(a)), Box::new(float_u.clone()))),
11509+ // avg : ... -> Rel (Float u) — a one-element relation.
1150611510 Box::new(Ty::Fun(
1150711511 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
11508− Box::new(float_u),
11512+ Box::new(Ty::Relation(Box::new(float_u))),
1150911513 )),
1151011514 ),
1151111515 },
@@ -11533,9 +11537,10 @@ impl Infer {
1153311537 unit_binops: vec![],
1153411538 ty: Ty::Fun(
1153511539 Box::new(Ty::Fun(Box::new(Ty::Var(a)), Box::new(Ty::Var(b)))),
11540+ // minOn : ... -> Rel b — a one-element relation.
1153611541 Box::new(Ty::Fun(
1153711542 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
11538− Box::new(Ty::Var(b)),
11543+ Box::new(Ty::Relation(Box::new(Ty::Var(b)))),
1153911544 )),
1154011545 ),
1154111546 },
@@ -11557,9 +11562,10 @@ impl Infer {
1155711562 unit_binops: vec![],
1155811563 ty: Ty::Fun(
1155911564 Box::new(Ty::Fun(Box::new(Ty::Var(a)), Box::new(Ty::Var(b)))),
11565+ // maxOn : ... -> Rel b — a one-element relation.
1156011566 Box::new(Ty::Fun(
1156111567 Box::new(Ty::Relation(Box::new(Ty::Var(a)))),
11562− Box::new(Ty::Var(b)),
11568+ Box::new(Ty::Relation(Box::new(Ty::Var(b)))),
1156311569 )),
1156411570 ),
1156511571 },
crates/knot-jit/tests/numeric_text.rs+1−1
@@ -123,7 +123,7 @@ fn elem_membership() {
123123
124124#[test]
125125fn count_where() {
126− assert_show("base.countWhere (\\x -> x > 2) [1 2 3 4]", "2");
126+ assert_show("base.countWhere (\\x -> x > 2) [1 2 3 4]", "[2]"); // countWhere is a one-element relation now
127127}
128128
129129// ── Record / misc ──────────────────────────────────────────────────────────
crates/knot-jit/tests/persistence.rs+2−2
@@ -42,8 +42,8 @@ Rel C *cs
4242 base.println (base.show (base.count rows))
4343 base.println (base.show (base.sum (base.map (\c -> c.n) rows)))
4444 yield {})"#,
45− // count is a one-element relation now (Rel Int), so it shows as [3].
46− "\"[3]\"\n\"6\"\n{}",
45+ // count and sum are one-element relations now (Rel Int) — [3] and [6].
46+ "\"[3]\"\n\"[6]\"\n{}",
4747 );
4848}
4949
crates/knot-jit/tests/relations.rs+1−1
@@ -95,7 +95,7 @@ fn count_empty() {
9595
9696#[test]
9797fn count_where() {
98− assert_show("base.countWhere (\\n -> n % 2 == 0) [1 2 3 4 5 6]", "3");
98+ assert_show("base.countWhere (\\n -> n % 2 == 0) [1 2 3 4 5 6]", "[3]"); // countWhere is a one-element relation now
9999}
100100
101101#[test]