@@ -3695,7 +3695,13 @@ enum ExKind<'a> {
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.
3703+ /// `List <scalar>` — a Cons/Nil chain.
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!(
3717+ ExKind::Rel("Bool")
3718+ | ExKind::Vec("Bool")
3719+ | ExKind::List("Bool")
3711− ExKind::Rel("Bool") | ExKind::Refined("Bool")
3720+ | ExKind::Refined("Bool")
@@ -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)
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(),
@@ -3725,8 +3736,8 @@ impl<'a> ExKind<'a> {
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();
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();
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));
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
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);
3895+ _ => unreachable!(),
3869− return if is_maybe { make_just(rel) } else { rel };
3897+ return if is_maybe { make_just(v) } else { v };
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)
3941+ ExKind::Rel(_) | ExKind::Vec(_) | ExKind::List(_) => {
3913− ExKind::Rel(_) => unreachable!("handled above"),
3942+ unreachable!("handled above")
3916 if is_maybe {
3946 if is_maybe {