on master help sign in
Let Vec, List, and Rel `#`-arguments share the collection flag syntax
0653e18iko
crates/knot-compiler/src/codegen.rs+7−3
@@ -680,8 +680,12 @@ fn scalar_override_type(
680 // unit-agnostic, classify by the base scalar.
680 // unit-agnostic, classify by the base scalar.
681 name if name.starts_with("Int ") => "Int".to_string(),
681 name if name.starts_with("Int ") => "Int".to_string(),
682 name if name.starts_with("Float ") => "Float".to_string(),
682 name if name.starts_with("Float ") => "Float".to_string(),
683− name if name.starts_with("Rel ") => {
683+ name if name.starts_with("Rel ")
684+ || name.starts_with("Vec ")
685+ || name.starts_with("List ") =>
686+ {
687+ let container = name.split_whitespace().next().unwrap();
684− let elem = name[4..].trim();
688+ let elem = name[container.len()..].trim();
685 let elem = elem
689 let elem = elem
686 .strip_prefix('(')
690 .strip_prefix('(')
687 .and_then(|r| r.strip_suffix(')'))
691 .and_then(|r| r.strip_suffix(')'))
@@ -692,7 +696,7 @@ fn scalar_override_type(
692 e if e.starts_with("Float ") => "Float",
696 e if e.starts_with("Float ") => "Float",
693 _ => return None,
697 _ => return None,
694 };
698 };
695− format!("Rel {elem_base}")
699+ format!("{container} {elem_base}")
696 }
700 }
697 name => {
701 name => {
698 match type_aliases.get(name)? {
702 match type_aliases.get(name)? {
crates/knot-jit/tests/cli_overrides.rs+133
@@ -381,6 +381,139 @@ Rel (Int 1) #ports []
381 assert_ne!(code, 0, "expected element parse failure, stderr: {err}");
381 assert_ne!(code, 0, "expected element parse failure, stderr: {err}");
382}
382}
383
383
384+// ── Vec and List: same flag syntax as Rel, different value semantics ────────
385+
386+#[test]
387+fn rel_dedups_vec_keeps_order_and_dups() {
388+ // A Rel is a set: duplicate elements collapse. A Vec is ordered and keeps
389+ // duplicates. Same flag syntax (`--tags=a,b,a`), different value.
390+ let dir = TempDir::fresh("rel_dedup");
391+ build(
392+ &dir,
393+ "rel_dedup",
394+ r#"with {
395+Rel Text #tags []
396+}
397+(|
398+ base.println (base.show tags)
399+ yield {})"#,
400+ );
401+ let (out, err, code) = run_with_args(&dir, "rel_dedup", &["--tags=a,b,a"]);
402+ assert_eq!(code, 0, "stderr: {err}");
403+ assert_eq!(out.trim_end(), "\"[a, b]\"\n{}");
404+
405+ let dir = TempDir::fresh("vec_dups");
406+ build(
407+ &dir,
408+ "vec_dups",
409+ r#"with {
410+Vec Text #tags (base.the (Vec Text) [])
411+}
412+(|
413+ base.println (base.show tags)
414+ yield {})"#,
415+ );
416+ let (out, err, code) = run_with_args(&dir, "vec_dups", &["--tags=a,b,a"]);
417+ assert_eq!(code, 0, "stderr: {err}");
418+ assert_eq!(out.trim_end(), "\"[a, b, a]\"\n{}");
419+}
420+
421+#[test]
422+fn vec_int_repeated_flags() {
423+ let dir = TempDir::fresh("vec_int");
424+ build(
425+ &dir,
426+ "vec_int",
427+ r#"with {
428+Vec (Int 1) #nums (base.the (Vec (Int 1)) [])
429+}
430+(|
431+ base.println (base.show nums)
432+ yield {})"#,
433+ );
434+ let (out, err, code) = run_with_args(&dir, "vec_int", &["--nums=1,2", "--nums=3"]);
435+ assert_eq!(code, 0, "stderr: {err}");
436+ assert_eq!(out.trim_end(), "\"[1, 2, 3]\"\n{}");
437+}
438+
439+#[test]
440+fn list_text_builds_cons_chain() {
441+ let dir = TempDir::fresh("list_text");
442+ build(
443+ &dir,
444+ "list_text",
445+ r#"with {
446+List Text #tags (List.Nil {})
447+}
448+(|
449+ base.println (base.show tags)
450+ yield {})"#,
451+ );
452+ let (out, err, code) = run_with_args(&dir, "list_text", &["--tags=a,b"]);
453+ assert_eq!(code, 0, "stderr: {err}");
454+ assert_eq!(
455+ out.trim_end(),
456+ "\"Cons {head a tail Cons {head b tail Nil}}\"\n{}"
457+ );
458+}
459+
460+#[test]
461+fn list_int_empty_flag_is_nil() {
462+ let dir = TempDir::fresh("list_empty");
463+ build(
464+ &dir,
465+ "list_empty",
466+ r#"with {
467+List (Int 1) #nums (List.Nil {})
468+}
469+(|
470+ base.println (base.show nums)
471+ yield {})"#,
472+ );
473+ let (out, err, code) = run_with_args(&dir, "list_empty", &["--nums="]);
474+ assert_eq!(code, 0, "stderr: {err}");
475+ assert_eq!(out.trim_end(), "\"Nil\"\n{}");
476+}
477+
478+#[test]
479+fn vec_bool_bare_flag() {
480+ let dir = TempDir::fresh("vec_bool");
481+ build(
482+ &dir,
483+ "vec_bool",
484+ r#"with {
485+Vec Bool #flags (base.the (Vec Bool) [])
486+}
487+(|
488+ base.println (base.show flags)
489+ yield {})"#,
490+ );
491+ let (out, err, code) = run_with_args(&dir, "vec_bool", &["--flags"]);
492+ assert_eq!(code, 0, "stderr: {err}");
493+ assert_eq!(out.trim_end(), "\"[True]\"\n{}");
494+}
495+
496+#[test]
497+fn maybe_vec_override_and_nothing() {
498+ let dir = TempDir::fresh("maybe_vec");
499+ build(
500+ &dir,
501+ "maybe_vec",
502+ r#"with {
503+Maybe (Vec Text) #tags (Maybe.Nothing {})
504+}
505+(|
506+ base.println (base.show tags)
507+ yield {})"#,
508+ );
509+ let (out, err, code) = run_with_args(&dir, "maybe_vec", &["--tags=a,b"]);
510+ assert_eq!(code, 0, "stderr: {err}");
511+ assert_eq!(out.trim_end(), "\"Just {value [a, b]}\"\n{}");
512+ let (out, err, code) = run_with_args(&dir, "maybe_vec", &["--tags=Nothing"]);
513+ assert_eq!(code, 0, "stderr: {err}");
514+ assert_eq!(out.trim_end(), "\"Nothing\"\n{}");
515+}
516+
384// ── Maybe variants of the new types ─────────────────────────────────────────
517// ── Maybe variants of the new types ─────────────────────────────────────────
385
518
386#[test]
519#[test]
crates/knot-runtime/src/lib.rs+39−9
@@ -3695,7 +3695,13 @@ enum ExKind<'a> {
3695 Bytes,
3695 Bytes,
3696 Uuid,
3696 Uuid,
3697 /// Element scalar for `Rel <scalar>` ("Int" | "Float" | "Text" | "Bool").
3697 /// Element scalar for `Rel <scalar>` ("Int" | "Float" | "Text" | "Bool").
3698+ /// A relation is a SET: rows are deduplicated.
3698 Rel(&'a str),
3699 Rel(&'a str),
3700+ /// `Vec <scalar>` — ordered, allows duplicates. Shares the relation
3701+ /// representation but skips dedup.
3702+ Vec(&'a str),
3703+ /// `List <scalar>` — a Cons/Nil chain.
3704+ List(&'a str),
3699 /// All-nullary ADT: valid constructor names.
3705 /// All-nullary ADT: valid constructor names.
3700 Enum(&'a [String]),
3706 Enum(&'a [String]),
3701 /// Refined scalar: the base scalar ("Int" | "Float" | "Text" | "Bool").
3707 /// Refined scalar: the base scalar ("Int" | "Float" | "Text" | "Bool").
@@ -3708,7 +3714,10 @@ impl<'a> ExKind<'a> {
3708 fn is_bool(&self) -> bool {
3714 fn is_bool(&self) -> bool {
3709 matches!(
3715 matches!(
3710 self,
3716 self,
3717+ ExKind::Rel("Bool")
3718+ | ExKind::Vec("Bool")
3719+ | ExKind::List("Bool")
3711− ExKind::Rel("Bool") | ExKind::Refined("Bool")
3720+ | ExKind::Refined("Bool")
3712 )
3721 )
3713 }
3722 }
3714
3723
@@ -3717,7 +3726,9 @@ impl<'a> ExKind<'a> {
3717 match self {
3726 match self {
3718 ExKind::Bytes => "base64url-encoded Bytes".to_string(),
3727 ExKind::Bytes => "base64url-encoded Bytes".to_string(),
3719 ExKind::Uuid => "a Uuid (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)".to_string(),
3728 ExKind::Uuid => "a Uuid (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)".to_string(),
3729+ ExKind::Rel(e) | ExKind::Vec(e) | ExKind::List(e) => {
3720− ExKind::Rel(e) => format!("a comma-separated list of {}", e),
3730+ format!("a comma-separated list of {}", e)
3731+ }
3721 ExKind::Enum(ctors) => format!("one of: {}", ctors.join(", ")),
3732 ExKind::Enum(ctors) => format!("one of: {}", ctors.join(", ")),
3722 ExKind::Refined(b) => b.to_string(),
3733 ExKind::Refined(b) => b.to_string(),
3723 }
3734 }
@@ -3725,8 +3736,8 @@ impl<'a> ExKind<'a> {
3725}
3736}
3726
3737
3727/// Parse the compiler-emitted descriptor: `Bytes`, `Uuid`, `Rel <scalar>`,
3738/// Parse the compiler-emitted descriptor: `Bytes`, `Uuid`, `Rel <scalar>`,
3728−/// `enum:<Name>(<Ctor>,<Ctor>,…)`, `refined:<Name>(<base-scalar>)`, each
3739+/// `Vec <scalar>`, `List <scalar>`, `enum:<Name>(<Ctor>,<Ctor>,…)`,
3729−/// optionally prefixed by `Maybe `.
3740+/// `refined:<Name>(<base-scalar>)`, each optionally prefixed by `Maybe `.
3730fn parse_ex_descriptor(desc: &str) -> Option<(bool, ExKind<'_>)> {
3741fn parse_ex_descriptor(desc: &str) -> Option<(bool, ExKind<'_>)> {
3731 let (is_maybe, base) = match desc.strip_prefix("Maybe ") {
3742 let (is_maybe, base) = match desc.strip_prefix("Maybe ") {
3732 Some(rest) => (true, rest),
3743 Some(rest) => (true, rest),
@@ -3736,6 +3747,8 @@ fn parse_ex_descriptor(desc: &str) -> Option<(bool, ExKind<'_>)> {
3736 "Bytes" => ExKind::Bytes,
3747 "Bytes" => ExKind::Bytes,
3737 "Uuid" => ExKind::Uuid,
3748 "Uuid" => ExKind::Uuid,
3738 _ if base.starts_with("Rel ") => ExKind::Rel(&base[4..]),
3749 _ if base.starts_with("Rel ") => ExKind::Rel(&base[4..]),
3750+ _ if base.starts_with("Vec ") => ExKind::Vec(&base[4..]),
3751+ _ if base.starts_with("List ") => ExKind::List(&base[5..]),
3739 _ if base.starts_with("enum:") => {
3752 _ if base.starts_with("enum:") => {
3740 let rest = &base[5..];
3753 let rest = &base[5..];
3741 let open = rest.find('(')?;
3754 let open = rest.find('(')?;
@@ -3849,13 +3862,13 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
3849 return make_nothing();
3862 return make_nothing();
3850 }
3863 }
3851
3864
3852− if let ExKind::Rel(elem) = &kind {
3865+ if let ExKind::Rel(elem) | ExKind::Vec(elem) | ExKind::List(elem) = &kind {
3853 if values.is_empty() {
3866 if values.is_empty() {
3854 return std::ptr::null_mut();
3867 return std::ptr::null_mut();
3855 }
3868 }
3856 // Each occurrence may be comma-separated; an empty value
3869 // Each occurrence may be comma-separated; an empty value
3857 // (`--tag=`) contributes no elements, so `--tag=` alone yields
3870 // (`--tag=`) contributes no elements, so `--tag=` alone yields
3858− // the empty relation.
3871+ // the empty collection.
3859 let mut rows: Vec<*mut Value> = Vec::new();
3872 let mut rows: Vec<*mut Value> = Vec::new();
3860 for occ in &values {
3873 for occ in &values {
3861 for piece in occ.split(',') {
3874 for piece in occ.split(',') {
@@ -3865,8 +3878,23 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
3865 rows.push(parse_rel_element(name, piece, elem));
3878 rows.push(parse_rel_element(name, piece, elem));
3866 }
3879 }
3867 }
3880 }
3881+ let v = match &kind {
3882+ // A relation is a set: dedup.
3883+ ExKind::Rel(_) => alloc(Value::Relation(in_memory_dedup(rows))),
3884+ // A vec is ordered and allows duplicates: same representation, no
3885+ // dedup.
3868− let rel = alloc(Value::Relation(rows));
3886+ ExKind::Vec(_) => alloc(Value::Relation(rows)),
3887+ // A list is a Cons/Nil chain, built tail-first.
3888+ ExKind::List(_) => {
3889+ let mut acc = list_nil();
3890+ for row in rows.into_iter().rev() {
3891+ acc = list_cons(row, acc);
3892+ }
3893+ acc
3894+ }
3895+ _ => unreachable!(),
3896+ };
3869− return if is_maybe { make_just(rel) } else { rel };
3897+ return if is_maybe { make_just(v) } else { v };
3870 }
3898 }
3871
3899
3872 // Scalar-shaped kinds: last occurrence wins.
3900 // Scalar-shaped kinds: last occurrence wins.
@@ -3910,7 +3938,9 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
3910 // by generated code after this returns.
3938 // by generated code after this returns.
3911 parse_rel_element(name, val_str, base)
3939 parse_rel_element(name, val_str, base)
3912 }
3940 }
3941+ ExKind::Rel(_) | ExKind::Vec(_) | ExKind::List(_) => {
3913− ExKind::Rel(_) => unreachable!("handled above"),
3942+ unreachable!("handled above")
3943+ }
3914 };
3944 };
3915
3945
3916 if is_maybe {
3946 if is_maybe {