@@ -237,6 +237,11 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
237237 // Type aliases for resolving response types in OpenAPI descriptors
238238 type_aliases: HashMap<String, ResolvedType>,
239239
240+ // CLI `#`-arg merged defaults: arg name → the concrete literal default any
241+ // scope supplies. A `_` (required) declaration whose name has a concrete
242+ // default elsewhere uses it instead of aborting when the flag is absent.
243+ cli_arg_defaults: HashMap<String, Option<String>>,
244+
240245 // Trampolines for user functions used as values: fn_name -> trampoline_func_id
241246 user_fn_trampolines: HashMap<String, FuncId>,
242247
@@ -1002,6 +1007,7 @@ fn compile_inner<M: cranelift_module::Module>(
10021007 .unwrap_or("knot");
10031008 cg.db_path = format!("{}.db", stem);
10041009 cg.source_schemas = type_env.source_schemas.clone();
1010+ cg.cli_arg_defaults = type_env.cli_arg_defaults.clone();
10051011 for (name, schema) in &type_env.source_schemas {
10061012 if schema.starts_with("_value:") {
10071013 cg.scalar_sources.insert(name.clone());
@@ -1722,6 +1728,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
17221728 route_entries: HashMap::new(),
17231729 fetch_route_entries: HashMap::new(),
17241730 type_aliases: HashMap::new(),
1731+ cli_arg_defaults: HashMap::new(),
17251732 user_fn_trampolines: HashMap::new(),
17261733 capture_env_in_progress: std::collections::HashSet::new(),
17271734 relational_do_spans: HashSet::new(),
@@ -1939,6 +1946,7 @@ impl<M: cranelift_module::Module> Codegen<M> {
19391946
19401947 // CLI constant overrides
19411948 self.declare_rt("knot_override_lookup", &[p, p, types::I32], &[p]);
1949+ self.declare_rt("knot_override_forbid", &[p, p], &[p]);
19421950 self.declare_rt("knot_override_lookup_ex", &[p, p, p, p], &[p]);
19431951 self.declare_rt("knot_override_unwrap_just", &[p], &[p]);
19441952 self.declare_rt("knot_override_check_refined", &[p, p, p, p, p, p], &[]);
@@ -6666,30 +6674,82 @@ ast::ExprKind::Atomic(inner) => {
66666674 builder.block_params(merge_block)[0]
66676675 }
66686676 None => {
6669− // Required: abort if the flag is missing.
6670− let missing_block = builder.create_block();
6671− let ok_block = builder.create_block();
6672− let is_null =
6673− builder.ins().icmp_imm(IntCC::Equal, override_val, 0);
6674− builder
6675− .ins()
6676− .brif(is_null, missing_block, &[], ok_block, &[]);
6677− builder.switch_to_block(missing_block);
6678− builder.seal_block(missing_block);
6679− let (msg_ptr, msg_len) = self.string_ptr(
6680− builder,
6681− &format!("missing required argument --{}", name),
6682− );
6683− let _ = self.call_rt(builder, "knot_todo", &[msg_ptr, msg_len]);
6684− builder.ins().jump(ok_block, &[]);
6685− builder.switch_to_block(ok_block);
6686− builder.seal_block(ok_block);
6687− override_val
6677+ // Required — UNLESS another scope declared this same
6678+ // `#name` with a concrete default (the agreement check
6679+ // merged it). Then the flag-or-default logic applies:
6680+ // the flag wins, else the merged default.
6681+ let merged_default = self
6682+ .cli_arg_defaults
6683+ .get(name.as_str())
6684+ .and_then(|d| d.as_ref())
6685+ .cloned();
6686+ match merged_default {
6687+ Some(lit_src) => {
6688+ // Parse the merged literal default into an
6689+ // expr and use the optional-arg default path.
6690+ let lexer = knot::lexer::Lexer::new(&lit_src);
6691+ let (tokens, _) = lexer.tokenize();
6692+ let parser =
6693+ knot::parser::Parser::new(lit_src.clone(), tokens);
6694+ let (dexpr, _) = parser.parse_file_expr();
6695+ let default_block = builder.create_block();
6696+ let override_block = builder.create_block();
6697+ let merge_block = builder.create_block();
6698+ builder.append_block_param(merge_block, self.ptr_type);
6699+ let is_null = builder
6700+ .ins()
6701+ .icmp_imm(IntCC::Equal, override_val, 0);
6702+ builder
6703+ .ins()
6704+ .brif(is_null, default_block, &[], override_block, &[]);
6705+
6706+ builder.switch_to_block(override_block);
6707+ builder.seal_block(override_block);
6708+ builder.ins().jump(merge_block, &[override_val.into()]);
6709+
6710+ builder.switch_to_block(default_block);
6711+ builder.seal_block(default_block);
6712+ let default_val =
6713+ self.compile_expr(builder, &dexpr, env, db);
6714+ builder.ins().jump(merge_block, &[default_val.into()]);
6715+
6716+ builder.switch_to_block(merge_block);
6717+ builder.seal_block(merge_block);
6718+ builder.block_params(merge_block)[0]
6719+ }
6720+ None => {
6721+ // Truly required: abort if the flag is missing.
6722+ let missing_block = builder.create_block();
6723+ let ok_block = builder.create_block();
6724+ let is_null = builder
6725+ .ins()
6726+ .icmp_imm(IntCC::Equal, override_val, 0);
6727+ builder
6728+ .ins()
6729+ .brif(is_null, missing_block, &[], ok_block, &[]);
6730+ builder.switch_to_block(missing_block);
6731+ builder.seal_block(missing_block);
6732+ let (msg_ptr, msg_len) = self.string_ptr(
6733+ builder,
6734+ &format!("missing required argument --{}", name),
6735+ );
6736+ let _ =
6737+ self.call_rt(builder, "knot_todo", &[msg_ptr, msg_len]);
6738+ builder.ins().jump(ok_block, &[]);
6739+ builder.switch_to_block(ok_block);
6740+ builder.seal_block(ok_block);
6741+ override_val
6742+ }
6743+ }
66886744 }
66896745 }
66906746 } else {
6691− // Non-overridable type — no CLI parser. Fall through to the
6692− // default (or error for required args).
6747+ // Non-overridable type — no CLI parser. Abort loudly if the
6748+ // flag was passed (rather than silently using the default),
6749+ // then fall through to the default (or error for required
6750+ // args).
6751+ let (name_ptr, name_len) = self.string_ptr(builder, name.as_str());
6752+ self.call_rt(builder, "knot_override_forbid", &[name_ptr, name_len]);
66936753 match default {
66946754 Some(d) => self.compile_expr(builder, d.as_ref(), env, db),
66956755 None => {