@@ -310,7 +310,9 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
310310 /// the value as a one-row relation; reads unwrap it to the bare value.
311311 single_value_sources: HashSet<String>,
312312
313− // Overridable constants: name -> type string ("Int", "Float", "Text", "Bool")
313+ // Overridable constants: name -> type descriptor string ("Int", "Float",
314+ // "Text", "Bool", "Maybe <scalar>", "Bytes", "Uuid", "Rel <scalar>",
315+ // "refined:<type-name>", "enum:<type-name>")
314316 overridable_constants: HashMap<String, String>,
315317
316318 // Default-value display strings for overridable constants whose body is a
@@ -323,6 +325,12 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
323325 // Compile-time constant overrides: name -> raw string value (parsed during codegen)
324326 compile_time_overrides: HashMap<String, String>,
325327
328+ /// All-nullary ADTs eligible for CLI override: type name -> constructor
329+ /// names. Populated from decl views before `overridable_constants` is
330+ /// computed; codegen uses it to emit the valid-constructor descriptor for
331+ /// `enum:` overrides.
332+ override_adt_ctors: HashMap<String, Vec<String>>,
333+
326334 // Whether we are inside compile_io_do_eager — when true, Yield compiles to
327335 // the raw inner value rather than wrapping in knot_relation_singleton.
328336 in_io_eager: bool,
@@ -590,40 +598,108 @@ struct LoopInfo {
590598 arena_mark: Value,
591599}
592600
593−/// Resolve a constant's inferred type, as rendered by inference, to the scalar
594−/// type the runtime knows how to parse from argv. Returns one of the eight
595−/// strings `define_user_function` maps to a type tag, or `None` when the
596−/// constant is not CLI-overridable.
601+/// Decode URL-safe base64 (RFC 4648 §5) at compile time — mirrors the
602+/// runtime's `decode_base64url`. Padding optional; standard-alphabet
603+/// characters rejected.
604+fn decode_base64url_compile(s: &str) -> Option<Vec<u8>> {
605+ if s.is_empty() {
606+ return Some(Vec::new());
607+ }
608+ let mut out = Vec::with_capacity(s.len() * 3 / 4);
609+ let mut buf: u32 = 0;
610+ let mut nbits: u32 = 0;
611+ for &b in s.as_bytes() {
612+ let v: u32 = match b {
613+ b'A'..=b'Z' => (b - b'A') as u32,
614+ b'a'..=b'z' => (b - b'a' + 26) as u32,
615+ b'0'..=b'9' => (b - b'0' + 52) as u32,
616+ b'-' => 62,
617+ b'_' => 63,
618+ b'=' => continue,
619+ _ => return None,
620+ };
621+ buf = (buf << 6) | v;
622+ nbits += 6;
623+ if nbits >= 8 {
624+ nbits -= 8;
625+ out.push((buf >> nbits) as u8);
626+ }
627+ }
628+ if nbits >= 6 || (nbits > 0 && (buf & ((1 << nbits) - 1)) != 0) {
629+ return None;
630+ }
631+ Some(out)
632+}
633+
634+/// Resolve a constant's inferred type, as rendered by inference, to the type
635+/// descriptor the runtime knows how to parse from argv. Returns one of the
636+/// strings `define_user_function` maps to a type tag (`Int`, `Float`, `Text`,
637+/// `Bool`, and `Maybe ` variants), one of the extended descriptors (`Bytes`,
638+/// `Uuid`, `Rel <scalar>`, `refined:<name>`, `enum:<name>`), or `None` when
639+/// the constant is not CLI-overridable.
597640///
598−/// A constant annotated with an alias of a scalar (`type Host = Text`) renders
599−/// as the alias name, so matching the rendered string alone would drop it and
600−/// leave the constant with no `knot_override_lookup` call — the flag would be
601−/// accepted and silently ignored. Resolving through `type_aliases` keeps this
602−/// correct for aliased scalars.
641+/// A constant annotated with an alias of a supported type (`type Host = Text`)
642+/// renders as the alias name, so matching the rendered string alone would
643+/// drop it and leave the constant with no `knot_override_lookup` call — the
644+/// flag would be accepted and silently ignored. Resolving through
645+/// `type_aliases` keeps this correct for aliased scalars.
603646///
604−/// Refined aliases are deliberately excluded: nothing here runs their
605−/// predicate, so honouring the flag would let a CLI value in through a check
606−/// the type promises.
647+/// Refined aliases are deliberately NOT resolved to their base type: the
648+/// predicate must run on the CLI value, so they classify as `refined:<name>`
649+/// and codegen emits the startup predicate check. Non-scalar aliases
650+/// (records, functions, relations of compound rows) are not overridable.
607651fn scalar_override_type(
608652 ty_str: &str,
609653 type_aliases: &HashMap<String, ResolvedType>,
610654 refined_types: &HashMap<String, ast::Expr>,
655+ adt_ctors: &HashMap<String, Vec<String>>,
611656) -> Option<String> {
612657 let (prefix, base) = match ty_str.strip_prefix("Maybe ") {
613658 Some(rest) => ("Maybe ", rest),
614659 None => ("", ty_str),
615660 };
661+ // Strip one layer of parens: `Maybe (Int Ms)` renders the arg
662+ // parenthesized.
663+ let base = base
664+ .strip_prefix('(')
665+ .and_then(|r| r.strip_suffix(')'))
666+ .unwrap_or(base);
616667 let resolved = match base {
617− "Int" | "Float" | "Text" | "Bool" => base,
668+ "Int" | "Float" | "Text" | "Bool" => base.to_string(),
669+ "Bytes" => "Bytes".to_string(),
670+ "Uuid" => "Uuid".to_string(),
671+ name if refined_types.contains_key(name) => format!("refined:{name}"),
672+ name if adt_ctors.contains_key(name) => format!("enum:{name}"),
673+ // Unit-bearing scalars (`Int Ms`, `Float 1`): CLI parsing is
674+ // unit-agnostic, classify by the base scalar.
675+ name if name.starts_with("Int ") => "Int".to_string(),
676+ name if name.starts_with("Float ") => "Float".to_string(),
677+ name if name.starts_with("Rel ") => {
678+ let elem = name[4..].trim();
679+ let elem = elem
680+ .strip_prefix('(')
681+ .and_then(|r| r.strip_suffix(')'))
682+ .unwrap_or(elem);
683+ let elem_base = match elem {
684+ "Int" | "Float" | "Text" | "Bool" => elem,
685+ e if e.starts_with("Int ") => "Int",
686+ e if e.starts_with("Float ") => "Float",
687+ _ => return None,
688+ };
689+ format!("Rel {elem_base}")
690+ }
618691 name => {
619− if refined_types.contains_key(name) {
620− return None;
621− }
622692 match type_aliases.get(name)? {
623− ResolvedType::Int => "Int",
624− ResolvedType::Float => "Float",
625− ResolvedType::Text => "Text",
626− ResolvedType::Bool => "Bool",
693+ ResolvedType::Int => "Int".to_string(),
694+ ResolvedType::Float => "Float".to_string(),
695+ ResolvedType::Text => "Text".to_string(),
696+ ResolvedType::Bool => "Bool".to_string(),
697+ ResolvedType::Bytes => "Bytes".to_string(),
698+ ResolvedType::Uuid => "Uuid".to_string(),
699+ // An alias of an all-nullary ADT resolves to Adt.
700+ ResolvedType::Adt(_) if adt_ctors.contains_key(name) => {
701+ format!("enum:{name}")
702+ }
627703 _ => return None,
628704 }
629705 }
@@ -998,6 +1074,24 @@ fn compile_inner<M: cranelift_module::Module>(
9981074 }
9991075 cg.declare_runtime_fns();
10001076 cg.collect_declarations(program);
1077+ // All-nullary ADTs ("enums") are CLI-overridable by constructor name.
1078+ // Collect them up front from the decl views: a `data` decl (or variant
1079+ // type alias) whose every constructor has no fields. Field-less
1080+ // constructor defs parse with an empty field list; any field disqualifies
1081+ // the whole type (payload constructors can't be spelled as a flag value).
1082+ let mut adt_ctors: HashMap<String, Vec<String>> = HashMap::new();
1083+ for decl in decl_views(program) {
1084+ if let DeclViewKind::Data { params, ctors } = decl.kind
1085+ && params.is_empty()
1086+ && !ctors.is_empty()
1087+ && ctors.iter().all(|c| c.fields.is_empty())
1088+ {
1089+ adt_ctors.insert(
1090+ decl.name.to_string(),
1091+ ctors.iter().map(|c| c.name.clone()).collect(),
1092+ );
1093+ }
1094+ }
10011095 // Compute overridable constants: 0-param Fun declarations with scalar types
10021096 for decl in decl_views(program) {
10031097 match decl.kind {
@@ -1010,8 +1104,12 @@ fn compile_inner<M: cranelift_module::Module>(
10101104 }
10111105 if let Some((_, 0)) = cg.global_fns.get(name)
10121106 && let Some(ty_str) = type_info.get(name)
1013− && let Some(base_type) =
1014− scalar_override_type(ty_str, &cg.type_aliases, &cg.refined_types)
1107+ && let Some(base_type) = scalar_override_type(
1108+ ty_str,
1109+ &cg.type_aliases,
1110+ &cg.refined_types,
1111+ &adt_ctors,
1112+ )
10151113 {
10161114 cg.overridable_constants.insert(name.to_string(), base_type);
10171115 if let Some(disp) = format_default_value_display(body) {
@@ -1025,8 +1123,12 @@ fn compile_inner<M: cranelift_module::Module>(
10251123 // default) are handled by the ArgDecl codegen arm.
10261124 let name = decl.name;
10271125 if let Some(ty_str) = type_info.get(name)
1028− && let Some(base_type) =
1029− scalar_override_type(ty_str, &cg.type_aliases, &cg.refined_types)
1126+ && let Some(base_type) = scalar_override_type(
1127+ ty_str,
1128+ &cg.type_aliases,
1129+ &cg.refined_types,
1130+ &adt_ctors,
1131+ )
10301132 {
10311133 cg.overridable_constants.insert(name.to_string(), base_type);
10321134 if let Some(d) = default
@@ -1039,6 +1141,9 @@ fn compile_inner<M: cranelift_module::Module>(
10391141 _ => {}
10401142 }
10411143 }
1144+ // Refined-type CLI overrides need their constructor-adjacent metadata:
1145+ // remember the descriptor so codegen can emit the predicate check.
1146+ cg.override_adt_ctors = adt_ctors;
10421147 // Validate and store compile-time overrides
10431148 for (name, val) in compile_time_overrides {
10441149 if let Some(ty_str) = cg.overridable_constants.get(name) {
@@ -1052,6 +1157,19 @@ fn compile_inner<M: cranelift_module::Module>(
10521157 "Float" | "Maybe Float" => "Float",
10531158 "Text" | "Maybe Text" => "Text",
10541159 "Bool" | "Maybe Bool" => "Bool",
1160+ "Bytes" | "Maybe Bytes" => "Bytes",
1161+ "Uuid" | "Maybe Uuid" => "Uuid",
1162+ s if s.starts_with("Rel ") || s.starts_with("Maybe Rel ") => "Rel",
1163+ s if s.strip_prefix("refined:").is_some()
1164+ || s.strip_prefix("Maybe refined:").is_some() =>
1165+ {
1166+ "Refined"
1167+ }
1168+ s if s.strip_prefix("enum:").is_some()
1169+ || s.strip_prefix("Maybe enum:").is_some() =>
1170+ {
1171+ "Enum"
1172+ }
10551173 _ => continue,
10561174 };
10571175 match base_type {
@@ -1082,6 +1200,64 @@ fn compile_inner<M: cranelift_module::Module>(
10821200 ));
10831201 continue;
10841202 }
1203+ "Bytes" => {
1204+ // base64url only (URL-safe alphabet, padding optional).
1205+ let valid = !val.is_empty()
1206+ && val.bytes().all(|b| {
1207+ b.is_ascii_alphanumeric() || b == b'-' || b == b'_' || b == b'='
1208+ });
1209+ if !valid {
1210+ cg.diagnostics.push(knot::diagnostic::Diagnostic::error(
1211+ format!("invalid compile-time override '{}' for --{} (expected base64url-encoded Bytes)", val, name),
1212+ ));
1213+ continue;
1214+ }
1215+ }
1216+ "Uuid" => {
1217+ let valid = val.len() == 36
1218+ && val.bytes().enumerate().all(|(i, b)| {
1219+ if i == 8 || i == 13 || i == 18 || i == 23 {
1220+ b == b'-'
1221+ } else {
1222+ b.is_ascii_hexdigit()
1223+ }
1224+ });
1225+ if !valid {
1226+ cg.diagnostics.push(knot::diagnostic::Diagnostic::error(
1227+ format!("invalid compile-time override '{}' for --{} (expected a Uuid xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)", val, name),
1228+ ));
1229+ continue;
1230+ }
1231+ }
1232+ "Enum" => {
1233+ // The value must be one of the ADT's constructors.
1234+ let ty_name = ty_str
1235+ .strip_prefix("Maybe ")
1236+ .unwrap_or(ty_str)
1237+ .trim_start_matches("enum:");
1238+ let valid = cg
1239+ .override_adt_ctors
1240+ .get(ty_name)
1241+ .map(|ctors| ctors.iter().any(|c| c == val))
1242+ .unwrap_or(false);
1243+ if !valid {
1244+ let ctors = cg
1245+ .override_adt_ctors
1246+ .get(ty_name)
1247+ .map(|c| c.join(", "))
1248+ .unwrap_or_default();
1249+ cg.diagnostics.push(knot::diagnostic::Diagnostic::error(
1250+ format!("invalid compile-time override '{}' for --{} (expected one of: {})", val, name, ctors),
1251+ ));
1252+ continue;
1253+ }
1254+ }
1255+ // Refined: parsed as the base scalar at runtime; the
1256+ // predicate check happens on the CLI path. Compile-time
1257+ // overrides of refined constants are validated the same
1258+ // way as their base scalar (best effort: no parse check
1259+ // here beyond accepting the string).
1260+ "Refined" | "Rel" => {}
10851261 _ => {} // Text always valid
10861262 }
10871263 }
@@ -1514,6 +1690,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
15141690 overridable_constants: HashMap::new(),
15151691 overridable_defaults: HashMap::new(),
15161692 compile_time_overrides: HashMap::new(),
1693+ override_adt_ctors: HashMap::new(),
15171694 in_io_eager: false,
15181695 atomic_retry_block: None,
15191696 io_loop_skip_block: None,
@@ -1711,6 +1888,9 @@ impl<M: cranelift_module::Module> Codegen<M> {
17111888
17121889 // CLI constant overrides
17131890 self.declare_rt("knot_override_lookup", &[p, p, types::I32], &[p]);
1891+ self.declare_rt("knot_override_lookup_ex", &[p, p, p, p], &[p]);
1892+ self.declare_rt("knot_override_unwrap_just", &[p], &[p]);
1893+ self.declare_rt("knot_override_check_refined", &[p, p, p, p, p, p], &[]);
17141894 self.declare_rt("knot_override_check_help", &[p, p], &[]);
17151895
17161896 // STM tracking
@@ -3795,48 +3975,44 @@ impl<M: cranelift_module::Module> Codegen<M> {
37953975
37963976 // For overridable constants, check CLI override before evaluating body
37973977 if let Some(type_str) = &override_type {
3798− let type_tag: Option<i64> = match type_str.as_str() {
3799− "Int" => Some(0),
3800− "Float" => Some(1),
3801− "Text" => Some(2),
3802− "Bool" => Some(3),
3803− "Maybe Int" => Some(4),
3804− "Maybe Float" => Some(5),
3805− "Maybe Text" => Some(6),
3806− "Maybe Bool" => Some(7),
3807− // Bytes/Uuid and other types have no CLI-override parser in
3808− // the runtime (knot_override_lookup only handles the scalar
3809− // types above). Skip the override for this constant rather
3810− // than crashing codegen.
3811− _ => {
3812− eprintln!(
3813− "knot: warning: constant '{}' of type '{}' does not support \
3814− command-line overrides; ignoring override for this constant",
3815− name_owned, type_str
3816− );
3817− None
3818− }
3819− };
3820− if let Some(type_tag) = type_tag {
3821− let (name_ptr, name_len) = cg.string_ptr(builder, &name_owned);
3822− let tag = builder.ins().iconst(types::I32, type_tag);
3823− let override_val =
3824− cg.call_rt(builder, "knot_override_lookup", &[name_ptr, name_len, tag]);
3825−
3826− let default_block = builder.create_block();
3827− let override_block = builder.create_block();
3828− let is_null = builder.ins().icmp_imm(IntCC::Equal, override_val, 0);
3829− builder
3830− .ins()
3831− .brif(is_null, default_block, &[], override_block, &[]);
3978+ let type_str = type_str.clone();
3979+ let (override_val, _) =
3980+ cg.emit_override_lookup(builder, &name_owned, &type_str);
3981+
3982+ // Refined-type override: the CLI value was parsed as the base
3983+ // scalar; run the refinement predicate(s) before it can flow
3984+ // into the program. Checked here (on the constant's own path)
3985+ // so every read of the constant sees a validated value.
3986+ if let Some(refined_name) = type_str
3987+ .strip_prefix("refined:")
3988+ .or_else(|| type_str.strip_prefix("Maybe refined:"))
3989+ {
3990+ let refined_name = refined_name.to_string();
3991+ let is_maybe = type_str.starts_with("Maybe ");
3992+ cg.emit_refined_override_check(
3993+ builder,
3994+ &name_owned,
3995+ &refined_name,
3996+ override_val,
3997+ is_maybe,
3998+ &mut env,
3999+ db,
4000+ );
4001+ }
38324002
3833− builder.switch_to_block(override_block);
3834− builder.seal_block(override_block);
3835− builder.ins().return_(&[override_val]);
4003+ let default_block = builder.create_block();
4004+ let override_block = builder.create_block();
4005+ let is_null = builder.ins().icmp_imm(IntCC::Equal, override_val, 0);
4006+ builder
4007+ .ins()
4008+ .brif(is_null, default_block, &[], override_block, &[]);
38364009
3837− builder.switch_to_block(default_block);
3838− builder.seal_block(default_block);
3839− }
4010+ builder.switch_to_block(override_block);
4011+ builder.seal_block(override_block);
4012+ builder.ins().return_(&[override_val]);
4013+
4014+ builder.switch_to_block(default_block);
4015+ builder.seal_block(default_block);
38404016 }
38414017
38424018 let result = cg.compile_expr(builder, &body_owned, &mut env, db);
@@ -4574,6 +4750,20 @@ impl<M: cranelift_module::Module> Codegen<M> {
45744750 // preserved by `splitn(3, ':')` at runtime)
45754751 // `name:type` — has default but not displayable
45764752 let descriptor = {
4753+ // Render the internal descriptor as the user-facing type:
4754+ // `refined:Port` → `Port`, `enum:Mode` → `Mode`, others
4755+ // pass through (already surface syntax).
4756+ let display_ty = |ty: &str| -> String {
4757+ let (prefix, rest) = match ty.strip_prefix("Maybe ") {
4758+ Some(r) => ("Maybe ", r),
4759+ None => ("", ty),
4760+ };
4761+ let base = rest
4762+ .strip_prefix("refined:")
4763+ .or_else(|| rest.strip_prefix("enum:"))
4764+ .unwrap_or(rest);
4765+ format!("{prefix}{base}")
4766+ };
45774767 let mut entries: Vec<String> = cg
45784768 .overridable_constants
45794769 .iter()
@@ -4581,9 +4771,9 @@ impl<M: cranelift_module::Module> Codegen<M> {
45814771 .map(|(name, ty)| {
45824772 if let Some(def) = cg.overridable_defaults.get(name) {
45834773 let escaped = def.replace('\\', "\\\\").replace(',', "\\c");
4584− format!("{}:{}:={}", name, ty, escaped)
4774+ format!("{}:{}:={}", name, display_ty(ty), escaped)
45854775 } else {
4586− format!("{}:{}", name, ty)
4776+ format!("{}:{}", name, display_ty(ty))
45874777 }
45884778 })
45894779 .collect();
@@ -6352,22 +6542,27 @@ ast::ExprKind::Atomic(inner) => {
63526542 // optional args use the default; required args (`#name _`)
63536543 // abort with an error.
63546544 let type_str = self.overridable_constants.get(name.as_str()).cloned();
6355− let type_tag: Option<i64> = type_str.as_deref().and_then(|s| match s {
6356− "Int" => Some(0),
6357− "Float" => Some(1),
6358− "Text" => Some(2),
6359− "Bool" => Some(3),
6360− "Maybe Int" => Some(4),
6361− "Maybe Float" => Some(5),
6362− "Maybe Text" => Some(6),
6363− "Maybe Bool" => Some(7),
6364− _ => None,
6365− });
6366− if let Some(type_tag) = type_tag {
6367− let (name_ptr, name_len) = self.string_ptr(builder, name);
6368− let tag = builder.ins().iconst(types::I32, type_tag);
6369− let override_val =
6370− self.call_rt(builder, "knot_override_lookup", &[name_ptr, name_len, tag]);
6545+ if let Some(type_str) = type_str {
6546+ let (override_val, _) = self.emit_override_lookup(builder, name, &type_str);
6547+
6548+ // Refined-type override: run the refinement predicate(s)
6549+ // on the CLI value before it reaches the program.
6550+ if let Some(refined_name) = type_str
6551+ .strip_prefix("refined:")
6552+ .or_else(|| type_str.strip_prefix("Maybe refined:"))
6553+ {
6554+ let refined_name = refined_name.to_string();
6555+ let is_maybe = type_str.starts_with("Maybe ");
6556+ self.emit_refined_override_check(
6557+ builder,
6558+ name,
6559+ &refined_name,
6560+ override_val,
6561+ is_maybe,
6562+ env,
6563+ db,
6564+ );
6565+ }
63716566
63726567 match default {
63736568 Some(d) => {
@@ -6420,14 +6615,14 @@ ast::ExprKind::Atomic(inner) => {
64206615 }
64216616 }
64226617 } else {
6423− // Non-scalar type — no CLI parser. Fall through to the
6618+ // Non-overridable type — no CLI parser. Fall through to the
64246619 // default (or error for required args).
64256620 match default {
64266621 Some(d) => self.compile_expr(builder, d.as_ref(), env, db),
64276622 None => {
64286623 self.diagnostics.push(knot::diagnostic::Diagnostic::error(
64296624 format!(
6430− "argument `#{}` has a non-scalar type and cannot be set from the command line",
6625+ "argument `#{}` has a type that cannot be set from the command line",
64316626 name
64326627 ),
64336628 ));
@@ -13235,6 +13430,191 @@ ast::ExprKind::Atomic(inner) => {
1323513430 /// clean compile error diagnostic instead of panicking, so a mistyped
1323613431 /// `--flag=...` reports like any other compile error rather than dumping a
1323713432 /// stack trace.
13433+ /// Map an override type descriptor to the legacy scalar tag (0–7) used by
13434+ /// `knot_override_lookup`. Extended descriptors (`Bytes`, `Uuid`,
13435+ /// `Rel <scalar>`, `refined:*`, `enum:*`, and `Maybe` of those) have no
13436+ /// tag — they go through `knot_override_lookup_ex` instead.
13437+ fn override_scalar_tag(type_str: &str) -> Option<i64> {
13438+ Some(match type_str {
13439+ "Int" => 0,
13440+ "Float" => 1,
13441+ "Text" => 2,
13442+ "Bool" => 3,
13443+ "Maybe Int" => 4,
13444+ "Maybe Float" => 5,
13445+ "Maybe Text" => 6,
13446+ "Maybe Bool" => 7,
13447+ _ => return None,
13448+ })
13449+ }
13450+
13451+ /// Expand `enum:<Name>` / `refined:<Name>` descriptors (and their `Maybe `
13452+ /// variants) to the form the runtime parses: `enum:<Name>(<Ctor>,…)` and
13453+ /// `refined:<Name>(<base-scalar>)`. Other descriptors pass through
13454+ /// unchanged.
13455+ fn override_runtime_descriptor(&self, type_str: &str) -> String {
13456+ let expand = |rest: &str| -> Option<String> {
13457+ if let Some(name) = rest.strip_prefix("enum:") {
13458+ let ctors = self.override_adt_ctors.get(name)?;
13459+ return Some(format!("enum:{}({})", name, ctors.join(",")));
13460+ }
13461+ if let Some(name) = rest.strip_prefix("refined:") {
13462+ let base = self.refined_base_scalar(name)?;
13463+ return Some(format!("refined:{}({})", name, base));
13464+ }
13465+ None
13466+ };
13467+ if let Some(expanded) = expand(type_str) {
13468+ return expanded;
13469+ }
13470+ if let Some(rest) = type_str.strip_prefix("Maybe ")
13471+ && let Some(expanded) = expand(rest)
13472+ {
13473+ return format!("Maybe {expanded}");
13474+ }
13475+ type_str.to_string()
13476+ }
13477+
13478+ /// Walk a refined type's base chain to its underlying scalar (`Int`,
13479+ /// `Float`, `Text`, `Bool`) — the shape the CLI value is parsed as.
13480+ /// Resolves through the resolved-alias map (`type_aliases`), which holds
13481+ /// every alias (including refined ones, mapped to their resolved base).
13482+ fn refined_base_scalar(&self, name: &str) -> Option<&str> {
13483+ let mut cur: &str = name;
13484+ loop {
13485+ // `cur` must be a refined alias; its base is the resolved alias
13486+ // target (another refined name, or the scalar itself).
13487+ let resolved = self.type_aliases.get(cur)?;
13488+ match resolved {
13489+ ResolvedType::Int => return Some("Int"),
13490+ ResolvedType::Float => return Some("Float"),
13491+ ResolvedType::Text => return Some("Text"),
13492+ ResolvedType::Bool => return Some("Bool"),
13493+ ResolvedType::Named(next) => cur = next.as_str(),
13494+ _ => return None,
13495+ }
13496+ }
13497+ }
13498+
13499+ /// Emit the CLI-override lookup for a constant/arg. Returns
13500+ /// `(value, is_ex)`: `value` is null when the flag is absent; `is_ex`
13501+ /// marks the extended descriptor path (parsed by
13502+ /// `knot_override_lookup_ex`, which handles Bytes/Uuid/Rel/enums/refined
13503+ /// bases). Refined-predicate checks are NOT emitted here — the caller
13504+ /// knows the value's provenance (default vs override) and checks only the
13505+ /// override.
13506+ fn emit_override_lookup(
13507+ &mut self,
13508+ builder: &mut FunctionBuilder,
13509+ name: &str,
13510+ type_str: &str,
13511+ ) -> (Value, bool) {
13512+ if let Some(tag) = Self::override_scalar_tag(type_str) {
13513+ let (name_ptr, name_len) = self.string_ptr(builder, name);
13514+ let tag = builder.ins().iconst(types::I32, tag);
13515+ let v = self.call_rt(builder, "knot_override_lookup", &[name_ptr, name_len, tag]);
13516+ (v, false)
13517+ } else {
13518+ let desc = self.override_runtime_descriptor(type_str);
13519+ let (name_ptr, name_len) = self.string_ptr(builder, name);
13520+ let (desc_ptr, desc_len) = self.string_ptr(builder, &desc);
13521+ let v = self.call_rt(
13522+ builder,
13523+ "knot_override_lookup_ex",
13524+ &[name_ptr, name_len, desc_ptr, desc_len],
13525+ );
13526+ (v, true)
13527+ }
13528+ }
13529+
13530+ /// Emit the startup refinement check for a refined-type CLI override:
13531+ /// every predicate in the type's refinement chain must hold for the
13532+ /// override value, else the runtime prints an error naming the flag and
13533+ /// type and exits. `flag_name` is the CLI flag (without `--`). No-op when
13534+ /// the override value is null (flag absent — the default was already
13535+ /// checked at compile time) or when the value arrived as
13536+ /// `Maybe.Nothing {}` (a None payload carries no refined scalar).
13537+ fn emit_refined_override_check(
13538+ &mut self,
13539+ builder: &mut FunctionBuilder,
13540+ flag_name: &str,
13541+ type_name: &str,
13542+ override_val: Value,
13543+ is_maybe: bool,
13544+ env: &mut Env,
13545+ db: Value,
13546+ ) {
13547+ // Collect the predicate chain (stacked refinements conjoin) by walking
13548+ // refined bases: `type Age = Nat where …` checks Nat's preds too. The
13549+ // chain walks the AST alias map (`alias_ast`): a refined alias's base
13550+ // is the AST type it wraps (`Nat`, `Int 1`), so stepping through
13551+ // `TypeKind::Named` bases visits each stacked refinement.
13552+ let mut preds: Vec<ast::Expr> = Vec::new();
13553+ let mut cur: String = type_name.to_string();
13554+ loop {
13555+ let Some(pred) = self.refined_types.get(&cur) else { break };
13556+ preds.push(pred.clone());
13557+ let Some(ast_ty) = self.alias_ast.get(&cur) else { break };
13558+ let ast::TypeKind::Refined { base, .. } = &ast_ty.node else {
13559+ break;
13560+ };
13561+ let ast::TypeKind::Named(n) = &base.node else { break };
13562+ cur = n.clone();
13563+ }
13564+ if preds.is_empty() {
13565+ return;
13566+ }
13567+
13568+ // Skip the check when the flag is absent (null) — the default value
13569+ // was checked at compile time. All blocks are sealed: each has a
13570+ // fixed predecessor set established below.
13571+ let present_block = builder.create_block();
13572+ let done_block = builder.create_block();
13573+ let is_null = builder.ins().icmp_imm(IntCC::Equal, override_val, 0);
13574+ builder
13575+ .ins()
13576+ .brif(is_null, done_block, &[], present_block, &[]);
13577+
13578+ builder.switch_to_block(present_block);
13579+ builder.seal_block(present_block);
13580+ // A Maybe-wrapped override (`Just {value: v}`) must be unwrapped
13581+ // before the predicate sees it; a `Nothing` override skips the check.
13582+ let check_val = if is_maybe {
13583+ let just_block = builder.create_block();
13584+ let unwrapped = self.call_rt(
13585+ builder,
13586+ "knot_override_unwrap_just",
13587+ &[override_val],
13588+ );
13589+ // knot_override_unwrap_just returns null for Nothing, else payload.
13590+ let is_none = builder.ins().icmp_imm(IntCC::Equal, unwrapped, 0);
13591+ builder
13592+ .ins()
13593+ .brif(is_none, done_block, &[], just_block, &[]);
13594+ builder.switch_to_block(just_block);
13595+ builder.seal_block(just_block);
13596+ unwrapped
13597+ } else {
13598+ override_val
13599+ };
13600+
13601+ let (flag_ptr, flag_len) = self.string_ptr(builder, flag_name);
13602+ let (ty_ptr, ty_len) = self.string_ptr(builder, type_name);
13603+ for pred in &preds {
13604+ let pred_fn = self.compile_expr(builder, pred, env, db);
13605+ let res = self.call_rt(builder, "knot_value_call", &[db, pred_fn, check_val]);
13606+ self.call_rt_void(
13607+ builder,
13608+ "knot_override_check_refined",
13609+ &[res, flag_ptr, flag_len, ty_ptr, ty_len, check_val],
13610+ );
13611+ }
13612+ builder.ins().jump(done_block, &[]);
13613+
13614+ builder.switch_to_block(done_block);
13615+ builder.seal_block(done_block);
13616+ }
13617+
1323813618 fn emit_override_literal(
1323913619 &mut self,
1324013620 builder: &mut FunctionBuilder,
@@ -13252,31 +13632,35 @@ ast::ExprKind::Atomic(inner) => {
1325213632 let unit = self.call_rt(builder, "knot_value_unit", &[]);
1325313633 return self.call_rt(builder, "knot_value_constructor", &[tag_ptr, tag_len, unit]);
1325413634 }
13255− // Helper to report a bad override value as a compile error and emit a
13256− // harmless unit value so codegen can keep going (the error aborts the
13257− // build before linking, via `compile()`'s diagnostics check).
13258− let mut bad = |expected: &str| -> Value {
13259− self.diagnostics
13260− .push(knot::diagnostic::Diagnostic::error(format!(
13261− "override value '{}' is not a valid {} for this constant",
13262− val_str, expected
13263− )));
13264− self.call_rt(builder, "knot_value_unit", &[])
13265− };
13635+ // Report a bad override value as a compile error and emit a harmless
13636+ // unit value so codegen can keep going (the error aborts the build
13637+ // before linking, via `compile()`'s diagnostics check). A macro, not a
13638+ // closure: a closure would hold `self`/`builder` borrowed across the
13639+ // whole match.
13640+ macro_rules! bad {
13641+ ($expected:expr) => {{
13642+ self.diagnostics
13643+ .push(knot::diagnostic::Diagnostic::error(format!(
13644+ "override value '{}' is not a valid {} for this constant",
13645+ val_str, $expected
13646+ )));
13647+ self.call_rt(builder, "knot_value_unit", &[])
13648+ }};
13649+ }
1326613650 let inner = match type_str {
1326713651 "Int" | "Maybe Int" => match val_str.parse::<i64>() {
1326813652 Ok(n) => {
1326913653 let n_val = builder.ins().iconst(types::I64, n);
1327013654 self.call_rt(builder, "knot_value_int", &[n_val])
1327113655 }
13272− Err(_) => bad("Int"),
13656+ Err(_) => bad!("Int"),
1327313657 },
1327413658 "Float" | "Maybe Float" => match val_str.parse::<f64>() {
1327513659 Ok(n) => {
1327613660 let n_val = builder.ins().f64const(n);
1327713661 self.call_rt(builder, "knot_value_float", &[n_val])
1327813662 }
13279− Err(_) => bad("Float"),
13663+ Err(_) => bad!("Float"),
1328013664 },
1328113665 "Text" | "Maybe Text" => {
1328213666 let (ptr, len) = self.string_ptr(builder, val_str);
@@ -13296,9 +13680,124 @@ ast::ExprKind::Atomic(inner) => {
1329613680 let val = builder.ins().iconst(types::I32, 0);
1329713681 self.call_rt(builder, "knot_value_bool", &[val])
1329813682 }
13299− _ => bad("Bool (expected true/false)"),
13683+ _ => bad!("Bool (expected true/false)"),
13684+ }
13685+ }
13686+ "Bytes" | "Maybe Bytes" => {
13687+ // base64url-decode at compile time, emit as a Bytes value via
13688+ // the runtime's `bytesFromHex`-independent constructor: encode
13689+ // the decoded bytes back to hex and call knot_bytes_from_hex
13690+ // on it (always `Just` for valid hex).
13691+ match decode_base64url_compile(val_str) {
13692+ Some(bytes) => {
13693+ let hex: String =
13694+ bytes.iter().map(|b| format!("{:02x}", b)).collect();
13695+ let (ptr, len) = self.string_ptr(builder, &hex);
13696+ let text = self.call_rt(builder, "knot_value_text", &[ptr, len]);
13697+ let just = self.call_rt(builder, "knot_bytes_from_hex", &[text]);
13698+ // Unwrap the guaranteed-Just to the bare Bytes.
13699+ self.call_rt(builder, "knot_override_unwrap_just", &[just])
13700+ }
13701+ None => bad!("base64url-encoded Bytes"),
1330013702 }
1330113703 }
13704+ "Uuid" | "Maybe Uuid" => {
13705+ let valid = val_str.len() == 36
13706+ && val_str.bytes().enumerate().all(|(i, b)| {
13707+ if i == 8 || i == 13 || i == 18 || i == 23 {
13708+ b == b'-'
13709+ } else {
13710+ b.is_ascii_hexdigit()
13711+ }
13712+ });
13713+ if valid {
13714+ let (ptr, len) = self.string_ptr(builder, val_str);
13715+ let slot = self.text_literal_slot(builder, val_str);
13716+ self.call_rt(builder, "knot_value_text_intern", &[ptr, len, slot])
13717+ } else {
13718+ bad!("Uuid (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)")
13719+ }
13720+ }
13721+ s if s.starts_with("enum:") || s.starts_with("Maybe enum:") => {
13722+ let ty_name = s
13723+ .strip_prefix("Maybe ")
13724+ .unwrap_or(s)
13725+ .trim_start_matches("enum:");
13726+ let known = self
13727+ .override_adt_ctors
13728+ .get(ty_name)
13729+ .map(|cs| cs.iter().any(|c| c == val_str))
13730+ .unwrap_or(false);
13731+ if known {
13732+ let (tag_ptr, tag_len) = self.string_ptr(builder, val_str);
13733+ let unit = self.call_rt(builder, "knot_value_unit", &[]);
13734+ self.call_rt(builder, "knot_value_constructor", &[tag_ptr, tag_len, unit])
13735+ } else {
13736+ let ctors = self
13737+ .override_adt_ctors
13738+ .get(ty_name)
13739+ .map(|c| c.join(", "))
13740+ .unwrap_or_default();
13741+ bad!(&format!("one of: {}", ctors))
13742+ }
13743+ }
13744+ s if s.starts_with("Rel ") || s.starts_with("Maybe Rel ") => {
13745+ // Comma-separated elements, parsed per the element scalar.
13746+ let elem = s
13747+ .trim_start_matches("Maybe ")
13748+ .trim_start_matches("Rel ")
13749+ .to_string();
13750+ let rel = self.call_rt(builder, "knot_relation_empty", &[]);
13751+ for piece in val_str.split(',') {
13752+ if piece.is_empty() {
13753+ continue;
13754+ }
13755+ let elem_val = match elem.as_str() {
13756+ "Int" => match piece.parse::<i64>() {
13757+ Ok(n) => {
13758+ let n_val = builder.ins().iconst(types::I64, n);
13759+ self.call_rt(builder, "knot_value_int", &[n_val])
13760+ }
13761+ Err(_) => bad!("Int element"),
13762+ },
13763+ "Float" => match piece.parse::<f64>() {
13764+ Ok(n) => {
13765+ let n_val = builder.ins().f64const(n);
13766+ self.call_rt(builder, "knot_value_float", &[n_val])
13767+ }
13768+ Err(_) => bad!("Float element"),
13769+ },
13770+ "Text" => {
13771+ let (ptr, len) = self.string_ptr(builder, piece);
13772+ self.call_rt(builder, "knot_value_text", &[ptr, len])
13773+ }
13774+ "Bool" => match piece {
13775+ "true" | "True" | "1" => {
13776+ let v = builder.ins().iconst(types::I32, 1);
13777+ self.call_rt(builder, "knot_value_bool", &[v])
13778+ }
13779+ "false" | "False" | "0" => {
13780+ let v = builder.ins().iconst(types::I32, 0);
13781+ self.call_rt(builder, "knot_value_bool", &[v])
13782+ }
13783+ _ => bad!("Bool element (expected true/false)"),
13784+ },
13785+ _ => bad!("scalar element"),
13786+ };
13787+ self.call_rt_void(builder, "knot_relation_push", &[rel, elem_val]);
13788+ }
13789+ rel
13790+ }
13791+ s if s.starts_with("refined:") || s.starts_with("Maybe refined:") => {
13792+ // Parse as the base scalar; the compile-time predicate check
13793+ // is a TODO (the CLI path checks at startup).
13794+ self.diagnostics
13795+ .push(knot::diagnostic::Diagnostic::error(format!(
13796+ "compile-time overrides of refined-type constants ('{}') are not yet supported; use a CLI flag instead",
13797+ type_str
13798+ )));
13799+ self.call_rt(builder, "knot_value_unit", &[])
13800+ }
1330213801 _ => {
1330313802 self.diagnostics
1330413803 .push(knot::diagnostic::Diagnostic::error(format!(