@@ -310,7 +310,9 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
310 /// the value as a one-row relation; reads unwrap it to the bare value.
310 /// the value as a one-row relation; reads unwrap it to the bare value.
311 single_value_sources: HashSet<String>,
311 single_value_sources: HashSet<String>,
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>")
314 overridable_constants: HashMap<String, String>,
316 overridable_constants: HashMap<String, String>,
316 // Default-value display strings for overridable constants whose body is a
318 // Default-value display strings for overridable constants whose body is a
@@ -323,6 +325,12 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
323 // Compile-time constant overrides: name -> raw string value (parsed during codegen)
325 // Compile-time constant overrides: name -> raw string value (parsed during codegen)
324 compile_time_overrides: HashMap<String, String>,
326 compile_time_overrides: HashMap<String, String>,
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>>,
326 // Whether we are inside compile_io_do_eager — when true, Yield compiles to
334 // Whether we are inside compile_io_do_eager — when true, Yield compiles to
327 // the raw inner value rather than wrapping in knot_relation_singleton.
335 // the raw inner value rather than wrapping in knot_relation_singleton.
328 in_io_eager: bool,
336 in_io_eager: bool,
@@ -590,40 +598,108 @@ struct LoopInfo {
590 arena_mark: Value,
598 arena_mark: Value,
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>> {
606+ return Some(Vec::new());
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,
621+ buf = (buf << 6) | v;
625+ out.push((buf >> nbits) as u8);
628+ if nbits >= 6 || (nbits > 0 && (buf & ((1 << nbits) - 1)) != 0) {
593−/// Resolve a constant's inferred type, as rendered by inference, to the scalar
634+/// Resolve a constant's inferred type, as rendered by inference, to the type
594−/// type the runtime knows how to parse from argv. Returns one of the eight
635+/// descriptor the runtime knows how to parse from argv. Returns one of the
595−/// strings `define_user_function` maps to a type tag, or `None` when 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
596−/// constant is not CLI-overridable.
639+/// the constant is not CLI-overridable.
598−/// A constant annotated with an alias of a scalar (`type Host = Text`) renders
641+/// A constant annotated with an alias of a supported type (`type Host = Text`)
599−/// as the alias name, so matching the rendered string alone would drop it and
642+/// renders as the alias name, so matching the rendered string alone would
600−/// leave the constant with no `knot_override_lookup` call — the flag would be
643+/// drop it and leave the constant with no `knot_override_lookup` call — the
601−/// accepted and silently ignored. Resolving through `type_aliases` keeps this
644+/// flag would be accepted and silently ignored. Resolving through
602−/// correct for aliased scalars.
645+/// `type_aliases` keeps this correct for aliased scalars.
604−/// Refined aliases are deliberately excluded: nothing here runs their
647+/// Refined aliases are deliberately NOT resolved to their base type: the
605−/// predicate, so honouring the flag would let a CLI value in through a check
648+/// predicate must run on the CLI value, so they classify as `refined:<name>`
606−/// the type promises.
649+/// and codegen emits the startup predicate check. Non-scalar aliases
650+/// (records, functions, relations of compound rows) are not overridable.
607fn scalar_override_type(
651fn scalar_override_type(
608 ty_str: &str,
652 ty_str: &str,
609 type_aliases: &HashMap<String, ResolvedType>,
653 type_aliases: &HashMap<String, ResolvedType>,
610 refined_types: &HashMap<String, ast::Expr>,
654 refined_types: &HashMap<String, ast::Expr>,
655+ adt_ctors: &HashMap<String, Vec<String>>,
611) -> Option<String> {
656) -> Option<String> {
612 let (prefix, base) = match ty_str.strip_prefix("Maybe ") {
657 let (prefix, base) = match ty_str.strip_prefix("Maybe ") {
613 Some(rest) => ("Maybe ", rest),
658 Some(rest) => ("Maybe ", rest),
614 None => ("", ty_str),
659 None => ("", ty_str),
661+ // Strip one layer of parens: `Maybe (Int Ms)` renders the arg
665+ .and_then(|r| r.strip_suffix(')'))
616 let resolved = match base {
667 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();
681+ .and_then(|r| r.strip_suffix(')'))
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",
689+ format!("Rel {elem_base}")
618 name => {
691 name => {
619− if refined_types.contains_key(name) {
622 match type_aliases.get(name)? {
692 match type_aliases.get(name)? {
623− ResolvedType::Int => "Int",
693+ ResolvedType::Int => "Int".to_string(),
624− ResolvedType::Float => "Float",
694+ ResolvedType::Float => "Float".to_string(),
625− ResolvedType::Text => "Text",
695+ ResolvedType::Text => "Text".to_string(),
626− ResolvedType::Bool => "Bool",
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}")
627 _ => return None,
703 _ => return None,
@@ -998,6 +1074,24 @@ fn compile_inner<M: cranelift_module::Module>(
999 cg.declare_runtime_fns();
1075 cg.declare_runtime_fns();
1000 cg.collect_declarations(program);
1076 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())
1090+ decl.name.to_string(),
1091+ ctors.iter().map(|c| c.name.clone()).collect(),
1001 // Compute overridable constants: 0-param Fun declarations with scalar types
1095 // Compute overridable constants: 0-param Fun declarations with scalar types
1002 for decl in decl_views(program) {
1096 for decl in decl_views(program) {
1003 match decl.kind {
1097 match decl.kind {
@@ -1010,8 +1104,12 @@ fn compile_inner<M: cranelift_module::Module>(
1011 if let Some((_, 0)) = cg.global_fns.get(name)
1105 if let Some((_, 0)) = cg.global_fns.get(name)
1012 && let Some(ty_str) = type_info.get(name)
1106 && let Some(ty_str) = type_info.get(name)
1013− && let Some(base_type) =
1107+ && let Some(base_type) = scalar_override_type(
1014− scalar_override_type(ty_str, &cg.type_aliases, &cg.refined_types)
1110+ &cg.refined_types,
1016 cg.overridable_constants.insert(name.to_string(), base_type);
1114 cg.overridable_constants.insert(name.to_string(), base_type);
1017 if let Some(disp) = format_default_value_display(body) {
1115 if let Some(disp) = format_default_value_display(body) {
@@ -1025,8 +1123,12 @@ fn compile_inner<M: cranelift_module::Module>(
1025 // default) are handled by the ArgDecl codegen arm.
1123 // default) are handled by the ArgDecl codegen arm.
1026 let name = decl.name;
1124 let name = decl.name;
1027 if let Some(ty_str) = type_info.get(name)
1125 if let Some(ty_str) = type_info.get(name)
1028− && let Some(base_type) =
1126+ && let Some(base_type) = scalar_override_type(
1029− scalar_override_type(ty_str, &cg.type_aliases, &cg.refined_types)
1129+ &cg.refined_types,
1031 cg.overridable_constants.insert(name.to_string(), base_type);
1133 cg.overridable_constants.insert(name.to_string(), base_type);
1032 if let Some(d) = default
1134 if let Some(d) = default
@@ -1039,6 +1141,9 @@ fn compile_inner<M: cranelift_module::Module>(
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;
1042 // Validate and store compile-time overrides
1147 // Validate and store compile-time overrides
1043 for (name, val) in compile_time_overrides {
1148 for (name, val) in compile_time_overrides {
1044 if let Some(ty_str) = cg.overridable_constants.get(name) {
1149 if let Some(ty_str) = cg.overridable_constants.get(name) {
@@ -1052,6 +1157,19 @@ fn compile_inner<M: cranelift_module::Module>(
1052 "Float" | "Maybe Float" => "Float",
1157 "Float" | "Maybe Float" => "Float",
1053 "Text" | "Maybe Text" => "Text",
1158 "Text" | "Maybe Text" => "Text",
1054 "Bool" | "Maybe Bool" => "Bool",
1159 "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() =>
1168+ s if s.strip_prefix("enum:").is_some()
1169+ || s.strip_prefix("Maybe enum:").is_some() =>
1055 _ => continue,
1173 _ => continue,
1057 match base_type {
1175 match base_type {
@@ -1082,6 +1200,64 @@ fn compile_inner<M: cranelift_module::Module>(
1083 continue;
1201 continue;
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'='
1210+ cg.diagnostics.push(knot::diagnostic::Diagnostic::error(
1211+ format!("invalid compile-time override '{}' for --{} (expected base64url-encoded Bytes)", val, name),
1217+ let valid = val.len() == 36
1218+ && val.bytes().enumerate().all(|(i, b)| {
1219+ if i == 8 || i == 13 || i == 18 || i == 23 {
1222+ b.is_ascii_hexdigit()
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),
1233+ // The value must be one of the ADT's constructors.
1234+ let ty_name = ty_str
1235+ .strip_prefix("Maybe ")
1237+ .trim_start_matches("enum:");
1239+ .override_adt_ctors
1241+ .map(|ctors| ctors.iter().any(|c| c == val))
1245+ .override_adt_ctors
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),
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" => {}
1085 _ => {} // Text always valid
1261 _ => {} // Text always valid
@@ -1514,6 +1690,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
1514 overridable_constants: HashMap::new(),
1690 overridable_constants: HashMap::new(),
1515 overridable_defaults: HashMap::new(),
1691 overridable_defaults: HashMap::new(),
1516 compile_time_overrides: HashMap::new(),
1692 compile_time_overrides: HashMap::new(),
1693+ override_adt_ctors: HashMap::new(),
1517 in_io_eager: false,
1694 in_io_eager: false,
1518 atomic_retry_block: None,
1695 atomic_retry_block: None,
1519 io_loop_skip_block: None,
1696 io_loop_skip_block: None,
@@ -1711,6 +1888,9 @@ impl<M: cranelift_module::Module> Codegen<M> {
1712 // CLI constant overrides
1889 // CLI constant overrides
1713 self.declare_rt("knot_override_lookup", &[p, p, types::I32], &[p]);
1890 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], &[]);
1714 self.declare_rt("knot_override_check_help", &[p, p], &[]);
1894 self.declare_rt("knot_override_check_help", &[p, p], &[]);
1716 // STM tracking
1896 // STM tracking
@@ -3795,48 +3975,44 @@ impl<M: cranelift_module::Module> Codegen<M> {
3796 // For overridable constants, check CLI override before evaluating body
3976 // For overridable constants, check CLI override before evaluating body
3797 if let Some(type_str) = &override_type {
3977 if let Some(type_str) = &override_type {
3798− let type_tag: Option<i64> = match type_str.as_str() {
3978+ let type_str = type_str.clone();
3799− "Int" => Some(0),
3979+ let (override_val, _) =
3800− "Float" => Some(1),
3980+ cg.emit_override_lookup(builder, &name_owned, &type_str);
3801− "Text" => Some(2),
3981+
3802− "Bool" => Some(3),
3982+ // Refined-type override: the CLI value was parsed as the base
3803− "Maybe Int" => Some(4),
3983+ // scalar; run the refinement predicate(s) before it can flow
3804− "Maybe Float" => Some(5),
3984+ // into the program. Checked here (on the constant's own path)
3805− "Maybe Text" => Some(6),
3985+ // so every read of the constant sees a validated value.
3806− "Maybe Bool" => Some(7),
3986+ if let Some(refined_name) = type_str
3807− // Bytes/Uuid and other types have no CLI-override parser in
3987+ .strip_prefix("refined:")
3808− // the runtime (knot_override_lookup only handles the scalar
3988+ .or_else(|| type_str.strip_prefix("Maybe refined:"))
3809− // types above). Skip the override for this constant rather
3989+ {
3810− // than crashing codegen.
3990+ let refined_name = refined_name.to_string();
3811− _ => {
3991+ let is_maybe = type_str.starts_with("Maybe ");
3812− eprintln!(
3992+ cg.emit_refined_override_check(
3813− "knot: warning: constant '{}' of type '{}' does not support \
3993+ builder,
3814− command-line overrides; ignoring override for this constant",
3994+ &name_owned,
3815− name_owned, type_str
3995+ &refined_name,
3816− );
3996+ override_val,
3817− None
3997+ is_maybe,
3820− if let Some(type_tag) = type_tag {
4000+ );
3821− let (name_ptr, name_len) = cg.string_ptr(builder, &name_owned);
4001+ }
3822− let tag = builder.ins().iconst(types::I32, type_tag);
3824− cg.call_rt(builder, "knot_override_lookup", &[name_ptr, name_len, tag]);
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);
3831− .brif(is_null, default_block, &[], override_block, &[]);
3833− builder.switch_to_block(override_block);
4003+ let default_block = builder.create_block();
3834− builder.seal_block(override_block);
4004+ let override_block = builder.create_block();
3835− builder.ins().return_(&[override_val]);
4005+ let is_null = builder.ins().icmp_imm(IntCC::Equal, override_val, 0);
4008+ .brif(is_null, default_block, &[], override_block, &[]);
3837− builder.switch_to_block(default_block);
4010+ builder.switch_to_block(override_block);
3838− builder.seal_block(default_block);
4011+ builder.seal_block(override_block);
3839− }
4012+ builder.ins().return_(&[override_val]);
4014+ builder.switch_to_block(default_block);
4015+ builder.seal_block(default_block);
3842 let result = cg.compile_expr(builder, &body_owned, &mut env, db);
4018 let result = cg.compile_expr(builder, &body_owned, &mut env, db);
@@ -4574,6 +4750,20 @@ impl<M: cranelift_module::Module> Codegen<M> {
4574 // preserved by `splitn(3, ':')` at runtime)
4750 // preserved by `splitn(3, ':')` at runtime)
4575 // `name:type` — has default but not displayable
4751 // `name:type` — has default but not displayable
4576 let descriptor = {
4752 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),
4762+ .strip_prefix("refined:")
4763+ .or_else(|| rest.strip_prefix("enum:"))
4765+ format!("{prefix}{base}")
4577 let mut entries: Vec<String> = cg
4767 let mut entries: Vec<String> = cg
4578 .overridable_constants
4768 .overridable_constants
@@ -4581,9 +4771,9 @@ impl<M: cranelift_module::Module> Codegen<M> {
4581 .map(|(name, ty)| {
4771 .map(|(name, ty)| {
4582 if let Some(def) = cg.overridable_defaults.get(name) {
4772 if let Some(def) = cg.overridable_defaults.get(name) {
4583 let escaped = def.replace('\\', "\\\\").replace(',', "\\c");
4773 let escaped = def.replace('\\', "\\\\").replace(',', "\\c");
4584− format!("{}:{}:={}", name, ty, escaped)
4774+ format!("{}:{}:={}", name, display_ty(ty), escaped)
4585 } else {
4775 } else {
4586− format!("{}:{}", name, ty)
4776+ format!("{}:{}", name, display_ty(ty))
4589 .collect();
4779 .collect();
@@ -6352,22 +6542,27 @@ ast::ExprKind::Atomic(inner) => {
6352 // optional args use the default; required args (`#name _`)
6542 // optional args use the default; required args (`#name _`)
6353 // abort with an error.
6543 // abort with an error.
6354 let type_str = self.overridable_constants.get(name.as_str()).cloned();
6544 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 {
6545+ if let Some(type_str) = type_str {
6356− "Int" => Some(0),
6546+ let (override_val, _) = self.emit_override_lookup(builder, name, &type_str);
6357− "Float" => Some(1),
6547+
6358− "Text" => Some(2),
6548+ // Refined-type override: run the refinement predicate(s)
6359− "Bool" => Some(3),
6549+ // on the CLI value before it reaches the program.
6360− "Maybe Int" => Some(4),
6550+ if let Some(refined_name) = type_str
6361− "Maybe Float" => Some(5),
6551+ .strip_prefix("refined:")
6362− "Maybe Text" => Some(6),
6552+ .or_else(|| type_str.strip_prefix("Maybe refined:"))
6363− "Maybe Bool" => Some(7),
6553+ {
6364− _ => None,
6554+ let refined_name = refined_name.to_string();
6555+ let is_maybe = type_str.starts_with("Maybe ");
6556+ self.emit_refined_override_check(
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);
6370− self.call_rt(builder, "knot_override_lookup", &[name_ptr, name_len, tag]);
6372 match default {
6567 match default {
6373 Some(d) => {
6568 Some(d) => {
@@ -6420,14 +6615,14 @@ ast::ExprKind::Atomic(inner) => {
6422 } else {
6617 } else {
6423− // Non-scalar type — no CLI parser. Fall through to the
6618+ // Non-overridable type — no CLI parser. Fall through to the
6424 // default (or error for required args).
6619 // default (or error for required args).
6425 match default {
6620 match default {
6426 Some(d) => self.compile_expr(builder, d.as_ref(), env, db),
6621 Some(d) => self.compile_expr(builder, d.as_ref(), env, db),
6427 None => {
6622 None => {
6428 self.diagnostics.push(knot::diagnostic::Diagnostic::error(
6623 self.diagnostics.push(knot::diagnostic::Diagnostic::error(
6429 format!(
6624 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",
@@ -13235,6 +13430,191 @@ ast::ExprKind::Atomic(inner) => {
13235 /// clean compile error diagnostic instead of panicking, so a mistyped
13430 /// clean compile error diagnostic instead of panicking, so a mistyped
13236 /// `--flag=...` reports like any other compile error rather than dumping a
13431 /// `--flag=...` reports like any other compile error rather than dumping a
13237 /// stack trace.
13432 /// 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 {
13444+ "Maybe Float" => 5,
13445+ "Maybe Text" => 6,
13446+ "Maybe Bool" => 7,
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
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(",")));
13461+ if let Some(name) = rest.strip_prefix("refined:") {
13462+ let base = self.refined_base_scalar(name)?;
13463+ return Some(format!("refined:{}({})", name, base));
13467+ if let Some(expanded) = expand(type_str) {
13470+ if let Some(rest) = type_str.strip_prefix("Maybe ")
13471+ && let Some(expanded) = expand(rest)
13473+ return format!("Maybe {expanded}");
13475+ type_str.to_string()
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;
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)?;
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(),
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
13506+ fn emit_override_lookup(
13508+ builder: &mut FunctionBuilder,
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]);
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(
13523+ "knot_override_lookup_ex",
13524+ &[name_ptr, name_len, desc_ptr, desc_len],
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(
13539+ builder: &mut FunctionBuilder,
13542+ override_val: Value,
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();
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 {
13561+ let ast::TypeKind::Named(n) = &base.node else { break };
13564+ if preds.is_empty() {
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);
13576+ .brif(is_null, done_block, &[], present_block, &[]);
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(
13586+ "knot_override_unwrap_just",
13589+ // knot_override_unwrap_just returns null for Nothing, else payload.
13590+ let is_none = builder.ins().icmp_imm(IntCC::Equal, unwrapped, 0);
13593+ .brif(is_none, done_block, &[], just_block, &[]);
13594+ builder.switch_to_block(just_block);
13595+ builder.seal_block(just_block);
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(
13608+ "knot_override_check_refined",
13609+ &[res, flag_ptr, flag_len, ty_ptr, ty_len, check_val],
13612+ builder.ins().jump(done_block, &[]);
13614+ builder.switch_to_block(done_block);
13615+ builder.seal_block(done_block);
13238 fn emit_override_literal(
13618 fn emit_override_literal(
13239 &mut self,
13619 &mut self,
13240 builder: &mut FunctionBuilder,
13620 builder: &mut FunctionBuilder,
@@ -13252,31 +13632,35 @@ ast::ExprKind::Atomic(inner) => {
13252 let unit = self.call_rt(builder, "knot_value_unit", &[]);
13632 let unit = self.call_rt(builder, "knot_value_unit", &[]);
13253 return self.call_rt(builder, "knot_value_constructor", &[tag_ptr, tag_len, unit]);
13633 return self.call_rt(builder, "knot_value_constructor", &[tag_ptr, tag_len, unit]);
13255− // Helper to report a bad override value as a compile error and emit a
13635+ // Report a bad override value as a compile error and emit a harmless
13256− // harmless unit value so codegen can keep going (the error aborts the
13636+ // unit value so codegen can keep going (the error aborts the build
13257− // build before linking, via `compile()`'s diagnostics check).
13637+ // before linking, via `compile()`'s diagnostics check). A macro, not a
13258− let mut bad = |expected: &str| -> Value {
13638+ // closure: a closure would hold `self`/`builder` borrowed across the
13640+ macro_rules! bad {
13641+ ($expected:expr) => {{
13259− self.diagnostics
13642+ self.diagnostics
13260− .push(knot::diagnostic::Diagnostic::error(format!(
13643+ .push(knot::diagnostic::Diagnostic::error(format!(
13261− "override value '{}' is not a valid {} for this constant",
13644+ "override value '{}' is not a valid {} for this constant",
13262− val_str, expected
13645+ val_str, $expected
13264− self.call_rt(builder, "knot_value_unit", &[])
13647+ self.call_rt(builder, "knot_value_unit", &[])
13266 let inner = match type_str {
13650 let inner = match type_str {
13267 "Int" | "Maybe Int" => match val_str.parse::<i64>() {
13651 "Int" | "Maybe Int" => match val_str.parse::<i64>() {
13268 Ok(n) => {
13652 Ok(n) => {
13269 let n_val = builder.ins().iconst(types::I64, n);
13653 let n_val = builder.ins().iconst(types::I64, n);
13270 self.call_rt(builder, "knot_value_int", &[n_val])
13654 self.call_rt(builder, "knot_value_int", &[n_val])
13272− Err(_) => bad("Int"),
13656+ Err(_) => bad!("Int"),
13274 "Float" | "Maybe Float" => match val_str.parse::<f64>() {
13658 "Float" | "Maybe Float" => match val_str.parse::<f64>() {
13275 Ok(n) => {
13659 Ok(n) => {
13276 let n_val = builder.ins().f64const(n);
13660 let n_val = builder.ins().f64const(n);
13277 self.call_rt(builder, "knot_value_float", &[n_val])
13661 self.call_rt(builder, "knot_value_float", &[n_val])
13279− Err(_) => bad("Float"),
13663+ Err(_) => bad!("Float"),
13281 "Text" | "Maybe Text" => {
13665 "Text" | "Maybe Text" => {
13282 let (ptr, len) = self.string_ptr(builder, val_str);
13666 let (ptr, len) = self.string_ptr(builder, val_str);
@@ -13296,9 +13680,124 @@ ast::ExprKind::Atomic(inner) => {
13296 let val = builder.ins().iconst(types::I32, 0);
13680 let val = builder.ins().iconst(types::I32, 0);
13297 self.call_rt(builder, "knot_value_bool", &[val])
13681 self.call_rt(builder, "knot_value_bool", &[val])
13299− _ => bad("Bool (expected true/false)"),
13683+ _ => bad!("Bool (expected true/false)"),
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) {
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])
13701+ None => bad!("base64url-encoded Bytes"),
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 {
13710+ b.is_ascii_hexdigit()
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])
13718+ bad!("Uuid (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)")
13721+ s if s.starts_with("enum:") || s.starts_with("Maybe enum:") => {
13723+ .strip_prefix("Maybe ")
13725+ .trim_start_matches("enum:");
13727+ .override_adt_ctors
13729+ .map(|cs| cs.iter().any(|c| c == val_str))
13730+ .unwrap_or(false);
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])
13737+ .override_adt_ctors
13739+ .map(|c| c.join(", "))
13740+ .unwrap_or_default();
13741+ bad!(&format!("one of: {}", ctors))
13744+ s if s.starts_with("Rel ") || s.starts_with("Maybe Rel ") => {
13745+ // Comma-separated elements, parsed per the element scalar.
13747+ .trim_start_matches("Maybe ")
13748+ .trim_start_matches("Rel ")
13750+ let rel = self.call_rt(builder, "knot_relation_empty", &[]);
13751+ for piece in val_str.split(',') {
13752+ if piece.is_empty() {
13755+ let elem_val = match elem.as_str() {
13756+ "Int" => match piece.parse::<i64>() {
13758+ let n_val = builder.ins().iconst(types::I64, n);
13759+ self.call_rt(builder, "knot_value_int", &[n_val])
13761+ Err(_) => bad!("Int element"),
13763+ "Float" => match piece.parse::<f64>() {
13765+ let n_val = builder.ins().f64const(n);
13766+ self.call_rt(builder, "knot_value_float", &[n_val])
13768+ Err(_) => bad!("Float element"),
13771+ let (ptr, len) = self.string_ptr(builder, piece);
13772+ self.call_rt(builder, "knot_value_text", &[ptr, len])
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])
13779+ "false" | "False" | "0" => {
13780+ let v = builder.ins().iconst(types::I32, 0);
13781+ self.call_rt(builder, "knot_value_bool", &[v])
13783+ _ => bad!("Bool element (expected true/false)"),
13785+ _ => bad!("scalar element"),
13787+ self.call_rt_void(builder, "knot_relation_push", &[rel, elem_val]);
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).
13795+ .push(knot::diagnostic::Diagnostic::error(format!(
13796+ "compile-time overrides of refined-type constants ('{}') are not yet supported; use a CLI flag instead",
13799+ self.call_rt(builder, "knot_value_unit", &[])
13303 self.diagnostics
13802 self.diagnostics
13304 .push(knot::diagnostic::Diagnostic::error(format!(
13803 .push(knot::diagnostic::Diagnostic::error(format!(