@@ -3695,7 +3695,13 @@ enum ExKind<'a> {
36953695 Bytes,
36963696 Uuid,
36973697 /// Element scalar for `Rel <scalar>` ("Int" | "Float" | "Text" | "Bool").
3698+ /// A relation is a SET: rows are deduplicated.
36983699 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),
36993705 /// All-nullary ADT: valid constructor names.
37003706 Enum(&'a [String]),
37013707 /// Refined scalar: the base scalar ("Int" | "Float" | "Text" | "Bool").
@@ -3708,7 +3714,10 @@ impl<'a> ExKind<'a> {
37083714 fn is_bool(&self) -> bool {
37093715 matches!(
37103716 self,
3711− ExKind::Rel("Bool") | ExKind::Refined("Bool")
3717+ ExKind::Rel("Bool")
3718+ | ExKind::Vec("Bool")
3719+ | ExKind::List("Bool")
3720+ | ExKind::Refined("Bool")
37123721 )
37133722 }
37143723
@@ -3717,7 +3726,9 @@ impl<'a> ExKind<'a> {
37173726 match self {
37183727 ExKind::Bytes => "base64url-encoded Bytes".to_string(),
37193728 ExKind::Uuid => "a Uuid (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)".to_string(),
3720− ExKind::Rel(e) => format!("a comma-separated list of {}", e),
3729+ ExKind::Rel(e) | ExKind::Vec(e) | ExKind::List(e) => {
3730+ format!("a comma-separated list of {}", e)
3731+ }
37213732 ExKind::Enum(ctors) => format!("one of: {}", ctors.join(", ")),
37223733 ExKind::Refined(b) => b.to_string(),
37233734 }
@@ -3725,8 +3736,8 @@ impl<'a> ExKind<'a> {
37253736}
37263737
37273738/// Parse the compiler-emitted descriptor: `Bytes`, `Uuid`, `Rel <scalar>`,
3728−/// `enum:<Name>(<Ctor>,<Ctor>,…)`, `refined:<Name>(<base-scalar>)`, each
3729−/// optionally prefixed by `Maybe `.
3739+/// `Vec <scalar>`, `List <scalar>`, `enum:<Name>(<Ctor>,<Ctor>,…)`,
3740+/// `refined:<Name>(<base-scalar>)`, each optionally prefixed by `Maybe `.
37303741fn parse_ex_descriptor(desc: &str) -> Option<(bool, ExKind<'_>)> {
37313742 let (is_maybe, base) = match desc.strip_prefix("Maybe ") {
37323743 Some(rest) => (true, rest),
@@ -3736,6 +3747,8 @@ fn parse_ex_descriptor(desc: &str) -> Option<(bool, ExKind<'_>)> {
37363747 "Bytes" => ExKind::Bytes,
37373748 "Uuid" => ExKind::Uuid,
37383749 _ 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..]),
37393752 _ if base.starts_with("enum:") => {
37403753 let rest = &base[5..];
37413754 let open = rest.find('(')?;
@@ -3849,13 +3862,13 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
38493862 return make_nothing();
38503863 }
38513864
3852− if let ExKind::Rel(elem) = &kind {
3865+ if let ExKind::Rel(elem) | ExKind::Vec(elem) | ExKind::List(elem) = &kind {
38533866 if values.is_empty() {
38543867 return std::ptr::null_mut();
38553868 }
38563869 // Each occurrence may be comma-separated; an empty value
38573870 // (`--tag=`) contributes no elements, so `--tag=` alone yields
3858− // the empty relation.
3871+ // the empty collection.
38593872 let mut rows: Vec<*mut Value> = Vec::new();
38603873 for occ in &values {
38613874 for piece in occ.split(',') {
@@ -3865,8 +3878,23 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
38653878 rows.push(parse_rel_element(name, piece, elem));
38663879 }
38673880 }
3868− let rel = alloc(Value::Relation(rows));
3869− return if is_maybe { make_just(rel) } else { rel };
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.
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+ };
3897+ return if is_maybe { make_just(v) } else { v };
38703898 }
38713899
38723900 // Scalar-shaped kinds: last occurrence wins.
@@ -3910,7 +3938,9 @@ pub extern "C-unwind" fn knot_override_lookup_ex(
39103938 // by generated code after this returns.
39113939 parse_rel_element(name, val_str, base)
39123940 }
3913− ExKind::Rel(_) => unreachable!("handled above"),
3941+ ExKind::Rel(_) | ExKind::Vec(_) | ExKind::List(_) => {
3942+ unreachable!("handled above")
3943+ }
39143944 };
39153945
39163946 if is_maybe {