From 8e6afd7fbd6f1f0d2fe7eedc8f678597ae98dff9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 00:39:12 +0000 Subject: [PATCH 1/5] v2 CLI: --target is a closed choice over rostered TargetModels; usage refusals exit 2 v2.cli.compile_cli gains CliEmitTarget = Rust | C | Swift, each arm naming one rostered TargetModel. The plan's declared default (cli_default_emit_target) is Rust, the argv every existing caller spawns. An unknown or dangling --target is a typed parse refusal (cli_unknown_target / cli_target_missing_value). Plan refusals are now CliUsageRefused and exit exit_code_misuse (2); emit refusals stay exit_code_general_error (1), the split native_driver_plan_exit already makes. The seed's native-CLI harness follows the declared usage line and the no-entry control's new status. Also records the v2-native route's 7 GB remote-runner OOM as a recurring failure mode, with its receipts. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ceeds_the_default_remote_runner_memory.dag | 24 +++ .../stage0/src/cli_run/native_lane_runner.rs | 23 ++- src/v2/cli/compile_cli.dag | 162 +++++++++++++----- src/v2/test/claim/cli/v2_native_cli_test.dag | 108 +++++++++++- .../occurrence_file_attribution_test.dag | 6 +- 5 files changed, 270 insertions(+), 53 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag diff --git a/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag b/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag new file mode 100644 index 00000000000..0657619558a --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.v2_native_route_exceeds_the_default_remote_runner_memory + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data v2_native_route_exceeds_the_default_remote_runner_memory: RecurringFailureMode = RecurringFailureMode { + identity: "v2_native_route_exceeds_the_default_remote_runner_memory" as NonEmptyStr, + + receipts: [ + "neither route that runs the v2 emit pipeline over a real dag/ closure fits the default remote build runner's memory. The two routes are the seed interpreter evaluating v2.compiler.self_host.compiler_closure_emit, and the seed emitting and building v2.cli.compile_cli for //gunbc/instruments:v2-native-cli (INVALID STATE: the instrument that establishes the v2-exclusive door, and the only v2 route that emits a named module's closure, cannot execute on the executor sessions are directed to use. HARM: a session that asks the v2 route what it refuses for a module gets an OOM kill with no stage marker instead of a located diagnostic, so the v2 emitter's refusals for real modules stay unmeasured, and a v2 change that breaks the door is caught only on a larger host).", + + "RECEIPT, measured 2026-09-24/25 while sizing the approve-ios Swift target (dashboard node adhoc-1a935b07-173). Both runs were on the BuildBuddy linux/amd64 runner (7 GB RAM, 6 CPUs, no cgroup memory limit, so the seed refuses to start with HostBudgetUnreadable until the shell is placed in a cgroup; the probe capped memory.max at 6.8 GB). (1) Interpreter route: a probe calling emit_compiler_import_closure_from_ingest_located over the 90-file import closure of gunbc.auth.approval_device_wire was OOM-killed (137) for the rust, c and swift target models alike, after 322 s, 1174 s and 361 s. (2) Built-CLI route: gunbc test //gunbc/instruments:v2-native-cli was OOM-killed (137) after 748 s, while the seed was emitting src/v2/cli/compile_cli.dag in-process, right after compile.normalize and before any crate was assembled (BuildBuddy invocation 40ebdf1e-9699-43e2-9975-83a1f0cad1a6). Neither kill left a stage marker, and memory.peak read back empty.", + + "WHY THIS IS A FAILURE MODE AND NOT A SIZING NOTE. An executor too small for the instrument does not produce a typed refusal. It produces a SIGKILL that looks the same as a hang or a crash, and every reader has to rediscover it at the cost of an hour of remote time per attempt. The honest arms are to name the executor class the instrument requires and refuse on a smaller one, or to shrink the route's working set. Retrying on the same runner answers nothing.", + + "RUNG FOUND AT: silent -- nothing declares the memory an instrument needs, and nothing refuses to dispatch it to an executor that lacks it. ATTAINABLE CEILING: mechanically preventable -- an instrument row can declare its required executor memory, and dispatch can refuse an executor below it with a typed cause. NEXT-RUNG TRIGGER, stated as the capability: gunbc.instrument_targets rows carry a declared executor memory floor that the remote dispatch reads and refuses against, SUFFICIENT FOR an under-sized dispatch of //gunbc/instruments:v2-native-cli to refuse before building instead of being OOM-killed. Routing is with the parent lane (eager-owl-205).", + ], + + evidence: [ + DeclarationRef { module_path: "gunbc.instrument_targets", decl_name: "v2_native_cli_label", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.compiler.self_host.compiler_closure_emit", decl_name: "emit_compiler_import_closure_from_ingest_located", field: WholeDeclaration }, + ], +} diff --git a/src/v1/stage0/src/cli_run/native_lane_runner.rs b/src/v1/stage0/src/cli_run/native_lane_runner.rs index 01ac0be10b5..0f79f3169f2 100644 --- a/src/v1/stage0/src/cli_run/native_lane_runner.rs +++ b/src/v1/stage0/src/cli_run/native_lane_runner.rs @@ -148,9 +148,16 @@ const CLI_DOOR_EMIT_LIMITATION: &str = "translate_rejected_grounding_not_derived /// `exit_failure`, which is `ExitFailure { code: exit_code_general_error }` -- and /// `extdeps.process.posix_exit` declares that as `1`. So a refusal this harness pins takes exactly /// 1. Accepting any non-zero admitted `101` (a Rust panic's status) beside plausible stderr, which -/// is a crash wearing a refusal's clothes. Both door arms compare against this. +/// is a crash wearing a refusal's clothes. The emit arm compares against this; the no-entry +/// control is a parse refusal and compares against `CLI_DOOR_USAGE_REFUSAL_EXIT`. const CLI_DOOR_REFUSAL_EXIT: i32 = 1; +/// THE STATUS A PARSE REFUSAL TAKES, WHICH IS NOT THE EMIT REFUSAL'S. `v2_cli_exit` maps a +/// `CliUsageRefused` -- a command line that was not usable, decided before any walk -- to +/// `extdeps.process.gnu_bash_exit` `exit_code_misuse`, which is `2`, the same split +/// `v2.compiler.compile` `native_driver_plan_exit` makes. The no-entry control is such a refusal. +const CLI_DOOR_USAGE_REFUSAL_EXIT: i32 = 2; + /// THE THREE MARKERS OF THE CLI'S REFUSAL RECORD, named so each is required rather than sought. /// The rendered main prints `REFUSED: `; for an emit refusal that reason is /// `v2.cli.compile_cli`'s `the closure did not emit; diagnostic chain: | FATAL AT `. @@ -161,7 +168,7 @@ const CLI_REFUSAL_PREFIX: &str = "REFUSED: "; /// detail, a ` -- ` boundary, and the module's DECLARED usage line -- not the detail alone. const CLI_DETAIL_USAGE_BOUNDARY: &str = " -- "; const CLI_DOOR_DECLARED_USAGE: &str = - "usage: emit --entry --source-root [--source-root ]..."; + "usage: emit --entry --source-root [--source-root ]... [--target ]"; const CLI_EMIT_REFUSAL_HEAD: &str = "the closure did not emit; diagnostic chain: "; const CLI_FATAL_AT_MARKER: &str = " | FATAL AT "; @@ -1837,16 +1844,16 @@ fn walk_cli_door( root_arg.clone(), ], )?; - // THE SAME EXIT RULE AS THE EMIT ARM. `cli_no_entry` is a `CliRunRefused` like any other, so it - // exits `exit_code_general_error`; accepting any non-zero here would admit a panic or a spawn - // convention as the located refusal this arm exists to observe. + // THE PARSE-REFUSAL EXIT RULE. `cli_no_entry` is a `CliUsageRefused`, so it exits + // `exit_code_misuse`; accepting any non-zero here would admit a panic or a spawn convention as + // the located refusal this arm exists to observe. match refused.status { - Some(CLI_DOOR_REFUSAL_EXIT) => {} + Some(CLI_DOOR_USAGE_REFUSAL_EXIT) => {} other => { return Err(format!( "V2-NATIVE REFUSAL cause=NativeCliDoorRefusalNotDiscriminating — the built CLI \ - exited {other:?} on an argv naming no --entry, and this door's refusals exit \ - {CLI_DOOR_REFUSAL_EXIT}. A zero accepts the argv its own `cli_no_entry` arm exists \ + exited {other:?} on an argv naming no --entry, and this door's parse refusals \ + exit {CLI_DOOR_USAGE_REFUSAL_EXIT}. A zero accepts the argv its own `cli_no_entry` arm exists \ to refuse; a signal or any other status is not a refusal at all." )) } diff --git a/src/v2/cli/compile_cli.dag b/src/v2/cli/compile_cli.dag index 15c4f41b019..7ee54a5737d 100644 --- a/src/v2/cli/compile_cli.dag +++ b/src/v2/cli/compile_cli.dag @@ -1,14 +1,18 @@ module v2.cli.compile_cli import std.compiler_entry { CompilerEntryDriver, NativeCliDriver } -import std.process { ExitSuccess, ProcessExit, exit_failure } +import std.process { ExitFailure, ExitSuccess, ProcessExit, exit_failure } +import extdeps.process.gnu_bash_exit { exit_code_misuse } import std.algebra { Cons, Empty, FreeMonoid } import v2.compiler.name_resolve { Admission, ResolutionSubject } import v2.compiler.self_host.compiler_closure_emit { emit_compiler_import_closure_from_ingest_located } import v2.compiler.source_authority { SourceRootIngest } +import v2.extdeps.languages.c { c_target_model } import v2.extdeps.languages.rust { rust_target_model } +import v2.extdeps.languages.swift { swift_target_model } +import v2.std.compilers.target_model { TargetModel } import v2.std.algebra { fold_list, length, list_reverse } import v2.std.diagnostic { Accepted, NonEmptyDiagnostics, Rejected, diagnostics_fatal, diagnostics_fatal_reason } @@ -18,6 +22,7 @@ import v2.std.lens_verdict { } import v2.std.node { Symbol } import v2.std.qualified_name { qualified_name_from_dotted_string } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } import v2.std.text { String } // THE V2-NATIVE CLI. THIS IS THE COMPILER'S OWN FRONT DOOR, WRITTEN IN THE COMPILER'S OWN LANGUAGE. @@ -121,8 +126,61 @@ data compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver // only one whose realization exists today; a second verb is an arm here, which is a refusal that // stops compiling if a consumer forgets it. Opening this with a catch-all would make an unknown // verb indistinguishable from an unimplemented one. +// THE TARGET IS A CLOSED CHOICE, NOT A STRING. Each arm names one rostered `TargetModel` +// (`cli_emit_target_model`), so an operator can ask only for a target this CLI can actually hand +// to the emitter, and a misspelt one is refused where it was typed instead of reaching the emitter +// as a name it has no model for. Adding a target is one arm, one spelling row and one model arm; +// the exhaustive matches below refuse to compile until all three exist. +type CliEmitTarget + = CliTargetRust + | CliTargetC + | CliTargetSwift + +data cli_emit_targets: List = [CliTargetRust, CliTargetC, CliTargetSwift] + +fn cli_emit_target_spelling(t: CliEmitTarget) -> String { + match t { + CliTargetRust => "rust" + CliTargetC => "c" + CliTargetSwift => "swift" + } +} + +fn cli_emit_target_model(t: CliEmitTarget) -> TargetModel { + match t { + CliTargetRust => rust_target_model() + CliTargetC => c_target_model() + CliTargetSwift => swift_target_model() + } +} + +// THE DEFAULT IS A DECLARED ROW OF THE PLAN, NOT A CONSTANT INSIDE THE RUN. An emit that names no +// `--target` was always a Rust emit (the self-host instruments spawn exactly that argv), so the plan +// records Rust as its target, and `v2_cli_run` only ever reads the target out of the plan. +data cli_default_emit_target: CliEmitTarget = CliTargetRust + +fn cli_emit_target_of_spelling(spelling: String) -> Optional { + fold_list( + xs: cli_emit_targets, + empty: optional_absent(), + cons: fn(acc, t) { + if cli_emit_target_spelling(t: t) == spelling { optional_present(value: t) } else { acc } + } + ) +} + +fn cli_emit_target_roster_text() -> String { + fold_list( + xs: cli_emit_targets, + empty: "", + cons: fn(acc, t) { + if acc == "" { cli_emit_target_spelling(t: t) } else { concat(concat(acc, ", "), cli_emit_target_spelling(t: t)) } + } + ) +} + type CliVerb - = CliEmitClosure { entry: String, source_roots: FreeMonoid } + = CliEmitClosure { entry: String, source_roots: FreeMonoid, target: CliEmitTarget } // A PLAN IS PARSED OR IT IS REFUSED, and the refusal carries its own reason symbol rather than a // rendered sentence, so the host renders it once and a witness can discriminate on the CAUSE. A @@ -140,18 +198,21 @@ type CliPlan // cannot un-refuse it, so the FIRST cause is the one reported. type CliParseState = CliParseSeekVerb - | CliParseSeekOption { entry: String, roots: FreeMonoid } - | CliParseSeekRootValue { entry: String, roots: FreeMonoid } - | CliParseSeekEntryValue { entry: String, roots: FreeMonoid } + | CliParseSeekOption { entry: String, roots: FreeMonoid, target: CliEmitTarget } + | CliParseSeekRootValue { entry: String, roots: FreeMonoid, target: CliEmitTarget } + | CliParseSeekEntryValue { entry: String, roots: FreeMonoid, target: CliEmitTarget } + | CliParseSeekTargetValue { entry: String, roots: FreeMonoid } | CliParseFailed { reason: Symbol, detail: String } data cli_option_source_root: String = "--source-root" data cli_option_entry: String = "--entry" +data cli_option_target: String = "--target" + data cli_verb_emit: String = "emit" -data cli_usage: String = "usage: emit --entry --source-root [--source-root ]..." +data cli_usage: String = "usage: emit --entry --source-root [--source-root ]... [--target ]" fn cli_parse_step(state: CliParseState, arg: String) -> CliParseState { match state { @@ -159,82 +220,96 @@ fn cli_parse_step(state: CliParseState, arg: String) -> CliParseState { CliParseFailed { reason: reason, detail: detail } CliParseSeekVerb => if arg == cli_verb_emit { - CliParseSeekOption { entry: "", roots: Empty } + CliParseSeekOption { entry: "", roots: Empty, target: cli_default_emit_target } } else { CliParseFailed { reason: ^cli_unknown_verb, detail: concat(concat("unknown verb ", arg), concat(" -- ", cli_usage)) } } - CliParseSeekRootValue { entry: entry, roots: roots } => + CliParseSeekRootValue { entry: entry, roots: roots, target: target } => if cli_arg_is_option(arg: arg) { CliParseFailed { reason: ^cli_source_root_missing_value, detail: concat(cli_option_source_root, " takes a directory, and an option followed it") } } else { - CliParseSeekOption { entry: entry, roots: Cons { head: arg, tail: roots } } + CliParseSeekOption { entry: entry, roots: Cons { head: arg, tail: roots }, target: target } } - CliParseSeekEntryValue { entry: _, roots: roots } => + CliParseSeekEntryValue { entry: _, roots: roots, target: target } => if cli_arg_is_option(arg: arg) { CliParseFailed { reason: ^cli_entry_missing_value, detail: concat(cli_option_entry, " takes a module path, and an option followed it") } } else { - CliParseSeekOption { entry: arg, roots: roots } + CliParseSeekOption { entry: arg, roots: roots, target: target } + } + CliParseSeekTargetValue { entry: entry, roots: roots } => + if cli_arg_is_option(arg: arg) { + CliParseFailed { + reason: ^cli_target_missing_value, + detail: concat(cli_option_target, " takes a target name, and an option followed it") + } + } else { + match cli_emit_target_of_spelling(spelling: arg) { + Present { value: target } => CliParseSeekOption { entry: entry, roots: roots, target: target } + Absent => + CliParseFailed { + reason: ^cli_unknown_target, + detail: concat( + concat("unknown target ", arg), + concat(" -- the rostered targets are ", cli_emit_target_roster_text()) + ) + } + } } - CliParseSeekOption { entry: entry, roots: roots } => + CliParseSeekOption { entry: entry, roots: roots, target: target } => if arg == cli_option_source_root { - CliParseSeekRootValue { entry: entry, roots: roots } + CliParseSeekRootValue { entry: entry, roots: roots, target: target } } else { if arg == cli_option_entry { - CliParseSeekEntryValue { entry: entry, roots: roots } + CliParseSeekEntryValue { entry: entry, roots: roots, target: target } } else { - CliParseFailed { - reason: ^cli_unknown_option, - detail: concat(concat("unknown option ", arg), concat(" -- ", cli_usage)) + if arg == cli_option_target { + CliParseSeekTargetValue { entry: entry, roots: roots } + } else { + CliParseFailed { + reason: ^cli_unknown_option, + detail: concat(concat("unknown option ", arg), concat(" -- ", cli_usage)) + } } } } } } -// AN OPTION IN A VALUE POSITION IS THE DEFECT, NOT THE PARTICULAR OPTION THAT LANDED THERE. Testing -// only for the option whose value is being sought would accept `--source-root --entry v2.cli` as a -// source root literally named `--entry`, and the walk would then refuse on a directory nobody meant -// to name. So both value positions ask this one predicate and agree by construction. -// -// IT IS THE CLOSED OPTION SET RATHER THAN A `--` PREFIX TEST, deliberately. A prefix test is a guess -// about spelling that would also have to decide what a lone `--` means and what a negative number -// means; this list is the vocabulary this CLI actually has, so adding an option is a row here and -// forgetting to add it is a value position that silently accepts the new option as a value. That -// failure is bounded and visible where a mis-specified prefix rule is neither. fn cli_arg_is_option(arg: String) -> Bool { - (arg == cli_option_source_root) || (arg == cli_option_entry) + (arg == cli_option_source_root) || (arg == cli_option_entry) || (arg == cli_option_target) } -// THE END STATE IS WHERE THE INCOMPLETE FORMS BECOME REFUSALS. Running out of arguments while -// seeking the verb, while seeking a `--source-root` value, or having accepted the verb and no root -// at all are three DIFFERENT defects with three different fixes, so they are three reasons rather -// than one "bad usage". fn cli_parse_finish(state: CliParseState) -> CliPlan { match state { CliParseFailed { reason: reason, detail: detail } => CliPlanRefused { reason: reason, detail: detail } CliParseSeekVerb => CliPlanRefused { reason: ^cli_no_verb, detail: concat("no verb given -- ", cli_usage) } - CliParseSeekRootValue { entry: _, roots: _ } => + CliParseSeekRootValue { entry: _, roots: _, target: _ } => CliPlanRefused { reason: ^cli_source_root_missing_value, detail: concat(cli_option_source_root, " takes a directory, and none followed it") } - CliParseSeekEntryValue { entry: _, roots: _ } => + CliParseSeekEntryValue { entry: _, roots: _, target: _ } => CliPlanRefused { reason: ^cli_entry_missing_value, detail: concat(cli_option_entry, " takes a module path, and none followed it") } - CliParseSeekOption { entry: entry, roots: roots } => + CliParseSeekTargetValue { entry: _, roots: _ } => + CliPlanRefused { + reason: ^cli_target_missing_value, + detail: concat(cli_option_target, " takes a target name, and none followed it") + } + CliParseSeekOption { entry: entry, roots: roots, target: target } => if length(xs: roots) == 0 { CliPlanRefused { reason: ^cli_no_source_root, @@ -248,7 +323,7 @@ fn cli_parse_finish(state: CliParseState) -> CliPlan { } } else { CliPlanParsed { - verb: CliEmitClosure { entry: entry, source_roots: list_reverse(xs: roots) } + verb: CliEmitClosure { entry: entry, source_roots: list_reverse(xs: roots), target: target } } } } @@ -270,7 +345,7 @@ fn v2_cli_plan_source_roots(plan: CliPlan) -> FreeMonoid { CliPlanRefused { reason: _, detail: _ } => Empty CliPlanParsed { verb: verb } => match verb { - CliEmitClosure { entry: _, source_roots: roots } => roots + CliEmitClosure { entry: _, source_roots: roots, target: _ } => roots } } } @@ -278,6 +353,7 @@ fn v2_cli_plan_source_roots(plan: CliPlan) -> FreeMonoid { type CliRunOutcome = CliEmitted { text: String, closure_size: Int, layer_count: Int, ready_size: Int } | CliRunRefused { reason: Symbol, detail: String } + | CliUsageRefused { reason: Symbol, detail: String } // THE REFUSAL PRINTS ITS CAUSE, AND FOR ONE GENERATION IT DID NOT. The rejected arm already // CARRIED `d.head.reason` (now the FATAL reason -- the head was the first advisory that happened @@ -315,10 +391,10 @@ type CliRunOutcome fn v2_cli_run(plan: CliPlan, ingest: SourceRootIngest) -> CliRunOutcome { match plan { CliPlanRefused { reason: reason, detail: detail } => - CliRunRefused { reason: reason, detail: detail } + CliUsageRefused { reason: reason, detail: detail } CliPlanParsed { verb: verb } => match verb { - CliEmitClosure { entry: entry, source_roots: _ } => + CliEmitClosure { entry: entry, source_roots: _, target: target } => if length(xs: ingest) == 0 { CliRunRefused { reason: ^cli_empty_ingest, @@ -328,7 +404,7 @@ fn v2_cli_run(plan: CliPlan, ingest: SourceRootIngest) -> CliRunOutcome { let emitted = emit_compiler_import_closure_from_ingest_located( ingest: ingest, admission: v2_cli_closure_admission(entry: entry), - target: rust_target_model() + target: cli_emit_target_model(t: target) ) match emitted.outcome { Accepted { value: receipt, diagnostics: _ } => @@ -380,10 +456,15 @@ fn v2_cli_closure_admission(entry: String) -> Admission { // is an exit another process was OBSERVED to take), so the shim maps an arm to a number and decides // nothing. A shim that chose the status from whether stdout was empty would be a second // representation of the verdict, free to disagree with the outcome it was derived from. +// A plan refusal exits `exit_code_misuse` (2) and an emit refusal `exit_code_general_error` (1): +// "the invocation was not usable" and "the subject did not emit" ask the caller for different +// fixes, which is the split `v2.compiler.compile` `native_driver_plan_exit` already makes. fn v2_cli_exit(outcome: CliRunOutcome) -> ProcessExit { match outcome { CliEmitted { text: _, closure_size: _, layer_count: _, ready_size: _ } => ExitSuccess CliRunRefused { reason: _, detail: detail } => exit_failure(reason: detail) + CliUsageRefused { reason: _, detail: detail } => + ExitFailure { code: exit_code_misuse, reason: detail } } } @@ -391,5 +472,6 @@ fn v2_cli_outcome_text(outcome: CliRunOutcome) -> String { match outcome { CliEmitted { text: text, closure_size: _, layer_count: _, ready_size: _ } => text CliRunRefused { reason: _, detail: _ } => "" + CliUsageRefused { reason: _, detail: _ } => "" } } diff --git a/src/v2/test/claim/cli/v2_native_cli_test.dag b/src/v2/test/claim/cli/v2_native_cli_test.dag index 04daa154155..ce30eed74cc 100644 --- a/src/v2/test/claim/cli/v2_native_cli_test.dag +++ b/src/v2/test/claim/cli/v2_native_cli_test.dag @@ -2,6 +2,8 @@ module v2.test.cli.v2_native_cli import std.algebra { Cons, Empty, FreeMonoid } import std.process { ExitFailure, ExitSuccess } +import extdeps.process.gnu_bash_exit { exit_code_misuse } +import extdeps.process.posix_exit { exit_code_general_error } import v2.cli.compile_cli { CliEmitClosure, CliEmitted, @@ -10,6 +12,12 @@ import v2.cli.compile_cli { CliPlanRefused, CliRunOutcome, CliRunRefused, + CliEmitTarget, + CliTargetC, + CliTargetRust, + CliTargetSwift, + CliUsageRefused, + cli_emit_target_spelling, v2_cli_exit, v2_cli_parse, v2_cli_plan_source_roots, @@ -54,7 +62,7 @@ fn plan_first_root(plan: CliPlan) -> String { CliPlanRefused { reason: _, detail: _ } => "" CliPlanParsed { verb: verb } => match verb { - CliEmitClosure { entry: _, source_roots: roots } => + CliEmitClosure { entry: _, source_roots: roots, target: _ } => match list_head(xs: roots) { HeadAbsent => "" HeadFound { value: first } => first @@ -66,6 +74,7 @@ fn plan_first_root(plan: CliPlan) -> String { fn outcome_refusal_reason(outcome: CliRunOutcome) -> Symbol { match outcome { CliRunRefused { reason: reason, detail: _ } => reason + CliUsageRefused { reason: reason, detail: _ } => reason CliEmitted { text: _, closure_size: _, layer_count: _, ready_size: _ } => ^cli_outcome_was_not_refused } @@ -74,6 +83,7 @@ fn outcome_refusal_reason(outcome: CliRunOutcome) -> Symbol { fn outcome_emitted(outcome: CliRunOutcome) -> Bool { match outcome { CliRunRefused { reason: _, detail: _ } => false + CliUsageRefused { reason: _, detail: _ } => false CliEmitted { text: _, closure_size: _, layer_count: _, ready_size: _ } => true } } @@ -128,7 +138,7 @@ data argv_unknown_verb: FreeMonoid = Cons { data argv_unknown_option: FreeMonoid = Cons { head: "emit", - tail: Cons { head: "--target", tail: Cons { head: "rust", tail: Empty } } + tail: Cons { head: "--output", tail: Cons { head: "out.rs", tail: Empty } } } test fn the_emit_verb_with_one_root_parses_holds() -> Bool { @@ -212,6 +222,74 @@ test fn an_unknown_verb_refuses_holds() -> Bool { == ^cli_unknown_verb } +fn argv_emit_with_target(target: String) -> FreeMonoid { + Cons { + head: "emit", + tail: Cons { + head: "--entry", + tail: Cons { + head: "cli_probe", + tail: Cons { + head: "--source-root", + tail: Cons { head: "dag", tail: Cons { head: "--target", tail: Cons { head: target, tail: Empty } } } + } + } + } + } +} + +data argv_emit_unknown_target: FreeMonoid = argv_emit_with_target(target: "kotlin") + +data argv_emit_dangling_target: FreeMonoid = Cons { + head: "emit", + tail: Cons { + head: "--entry", + tail: Cons { head: "cli_probe", tail: Cons { head: "--source-root", tail: Cons { head: "dag", tail: Cons { head: "--target", tail: Empty } } } } + } +} + +fn plan_is_target(plan: CliPlan, expected: CliEmitTarget) -> Bool { + match plan { + CliPlanRefused { reason: _, detail: _ } => false + CliPlanParsed { verb: verb } => + match verb { + CliEmitClosure { entry: _, source_roots: _, target: target } => + cli_emit_target_spelling(t: target) == cli_emit_target_spelling(t: expected) + } + } +} + +// EACH ROSTERED SPELLING SELECTS ITS OWN ARM. Three rows rather than one because a parser that +// mapped every known spelling to the first arm would pass any single one of them. +test fn the_rust_target_spelling_selects_rust_holds() -> Bool { + plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "rust")), expected: CliTargetRust) +} + +test fn the_c_target_spelling_selects_c_holds() -> Bool { + plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "c")), expected: CliTargetC) +} + +test fn the_swift_target_spelling_selects_swift_holds() -> Bool { + plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "swift")), expected: CliTargetSwift) +} + +// NO --target IS THE PLAN'S DECLARED DEFAULT, which is the argv every existing caller spawns. +test fn an_emit_without_a_target_plans_the_declared_default_holds() -> Bool { + plan_is_target(plan: v2_cli_parse(argv: argv_of(items: argv_emit_one_root)), expected: CliTargetRust) +} + +// A TARGET OUTSIDE THE ROSTER REFUSES AT THE PARSE, before any root is walked -- it never reaches +// the emitter as a name the emitter has no model for. +test fn an_unknown_target_refuses_holds() -> Bool { + plan_refusal_reason(plan: v2_cli_parse(argv: argv_of(items: argv_emit_unknown_target))) + == ^cli_unknown_target +} + +test fn a_dangling_target_option_refuses_holds() -> Bool { + plan_refusal_reason(plan: v2_cli_parse(argv: argv_of(items: argv_emit_dangling_target))) + == ^cli_target_missing_value +} + test fn an_unknown_option_refuses_holds() -> Bool { plan_refusal_reason(plan: v2_cli_parse(argv: argv_of(items: argv_unknown_option))) == ^cli_unknown_option @@ -295,12 +373,33 @@ test fn a_refused_plan_keeps_its_cause_through_the_run_holds() -> Bool { // emits and compiles this module's whole closure -- that is compilation of the emit arm and not // execution of it, and the difference is exactly what this frontier records. -test fn a_refused_outcome_exits_failure_holds() -> Bool { +// A COMMAND LINE THAT WAS NOT USABLE EXITS 2, WORK THAT RAN AND REFUSED EXITS 1. The two are +// different terminations for the caller -- one means "fix the invocation", the other "the subject +// did not emit" -- so each row pins its exact code rather than any non-zero. +test fn a_refused_plan_exits_misuse_holds() -> Bool { match v2_cli_exit( outcome: v2_cli_run(plan: v2_cli_parse(argv: argv_of(items: argv_empty)), ingest: Empty) ) { ExitSuccess => false - ExitFailure { code: code, reason: _ } => code != 0 + ExitFailure { code: code, reason: _ } => code == exit_code_misuse + } +} + +test fn an_unknown_target_exits_misuse_holds() -> Bool { + match v2_cli_exit( + outcome: v2_cli_run(plan: v2_cli_parse(argv: argv_of(items: argv_emit_unknown_target)), ingest: Empty) + ) { + ExitSuccess => false + ExitFailure { code: code, reason: _ } => code == exit_code_misuse + } +} + +test fn an_emit_refusal_exits_general_error_holds() -> Bool { + match v2_cli_exit( + outcome: v2_cli_run(plan: v2_cli_parse(argv: argv_of(items: argv_emit_one_root)), ingest: Empty) + ) { + ExitSuccess => false + ExitFailure { code: code, reason: _ } => code == exit_code_general_error } } @@ -362,6 +461,7 @@ test fn a_trailing_token_refusal_locates_its_fatal_in_the_file_holds() -> Bool { CliRunRefused { reason: _, detail: detail } => string_contains(s: detail, pattern: "FATAL AT cli_probe_trailing_cu bytes ") && string_contains(s: detail, pattern: "parse_g0_tokens_remain @ cli_probe_trailing_cu bytes ") + CliUsageRefused { reason: _, detail: _ } => false CliEmitted { text: _, closure_size: _, layer_count: _, ready_size: _ } => false } } diff --git a/src/v2/test/claim/provenance/occurrence_file_attribution_test.dag b/src/v2/test/claim/provenance/occurrence_file_attribution_test.dag index 85e0caa715a..e366a1597f7 100644 --- a/src/v2/test/claim/provenance/occurrence_file_attribution_test.dag +++ b/src/v2/test/claim/provenance/occurrence_file_attribution_test.dag @@ -19,6 +19,8 @@ import v2.cli.compile_cli { CliEmitted, CliPlanParsed, CliRunRefused, + CliTargetRust, + CliUsageRefused, v2_cli_run } import v2.std.algebra { list_reverse, list_starts_with } @@ -270,13 +272,15 @@ fn door_probe_cli_detail() -> String { plan: CliPlanParsed { verb: CliEmitClosure { entry: "fixture.native_cli_door.door_probe", - source_roots: Empty + source_roots: Empty, + target: CliTargetRust } }, ingest: [door_probe_read()] ) { CliEmitted { text: _, closure_size: _, layer_count: _, ready_size: _ } => "" CliRunRefused { reason: _, detail: detail } => detail + CliUsageRefused { reason: _, detail: detail } => detail } } From fee0bcdaf7ad5317ee3d82befd9f6bc5860093b4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 01:07:56 +0000 Subject: [PATCH 2/5] RFM receipt: single v2 claim files, including an unchanged one from main, also OOM on the 7 GB runner Co-Authored-By: Claude Opus 5.5 (1M context) --- ...v2_native_route_exceeds_the_default_remote_runner_memory.dag | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag b/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag index 0657619558a..d95b57511ff 100644 --- a/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag +++ b/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag @@ -12,6 +12,8 @@ data v2_native_route_exceeds_the_default_remote_runner_memory: RecurringFailureM "RECEIPT, measured 2026-09-24/25 while sizing the approve-ios Swift target (dashboard node adhoc-1a935b07-173). Both runs were on the BuildBuddy linux/amd64 runner (7 GB RAM, 6 CPUs, no cgroup memory limit, so the seed refuses to start with HostBudgetUnreadable until the shell is placed in a cgroup; the probe capped memory.max at 6.8 GB). (1) Interpreter route: a probe calling emit_compiler_import_closure_from_ingest_located over the 90-file import closure of gunbc.auth.approval_device_wire was OOM-killed (137) for the rust, c and swift target models alike, after 322 s, 1174 s and 361 s. (2) Built-CLI route: gunbc test //gunbc/instruments:v2-native-cli was OOM-killed (137) after 748 s, while the seed was emitting src/v2/cli/compile_cli.dag in-process, right after compile.normalize and before any crate was assembled (BuildBuddy invocation 40ebdf1e-9699-43e2-9975-83a1f0cad1a6). Neither kill left a stage marker, and memory.peak read back empty.", + "RECEIPT, same executor, 2026-09-25: the limit is not specific to the two routes above. `gunbc run --claim-run` of single v2 claim files was OOM-killed as well -- src/v2/test/claim/cli/v2_native_cli_test.dag (oom_kill 4), and in one dispatch both a new Swift routing claim and the UNCHANGED src/v2/test/claim/emit/c_semantic_decl_routing_test.dag from main (oom_kill 5). So on this runner, no v2 claim a session changes can be executed before push. The required floor on a larger host is the first place those claims run.", + "WHY THIS IS A FAILURE MODE AND NOT A SIZING NOTE. An executor too small for the instrument does not produce a typed refusal. It produces a SIGKILL that looks the same as a hang or a crash, and every reader has to rediscover it at the cost of an hour of remote time per attempt. The honest arms are to name the executor class the instrument requires and refuse on a smaller one, or to shrink the route's working set. Retrying on the same runner answers nothing.", "RUNG FOUND AT: silent -- nothing declares the memory an instrument needs, and nothing refuses to dispatch it to an executor that lacks it. ATTAINABLE CEILING: mechanically preventable -- an instrument row can declare its required executor memory, and dispatch can refuse an executor below it with a typed cause. NEXT-RUNG TRIGGER, stated as the capability: gunbc.instrument_targets rows carry a declared executor memory floor that the remote dispatch reads and refuses against, SUFFICIENT FOR an under-sized dispatch of //gunbc/instruments:v2-native-cli to refuse before building instead of being OOM-killed. Routing is with the parent lane (eager-owl-205).", From 3490bac07f2cada189c2307fa4345e245406dc1e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 01:13:32 +0000 Subject: [PATCH 3/5] v2 CLI: roster Rust and C only; Swift waits on its target module being seed-emittable The first emit-build carrying v2.extdeps.languages.swift in the CLI closure failed rustc (E0308 at swift_bool_grounding, plus two truncated errors; CI run 36078559948). A rostered target's model is in the door's closure, so Swift joins the roster when that module emits cleanly, the trigger stated beside CliEmitTarget. The unknown-target claim now uses 'swift' as its discriminating input. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/cli/compile_cli.dag | 15 ++++++++++----- src/v2/test/claim/cli/v2_native_cli_test.dag | 8 ++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/v2/cli/compile_cli.dag b/src/v2/cli/compile_cli.dag index 7ee54a5737d..31f81175c82 100644 --- a/src/v2/cli/compile_cli.dag +++ b/src/v2/cli/compile_cli.dag @@ -11,7 +11,6 @@ import v2.compiler.self_host.compiler_closure_emit { import v2.compiler.source_authority { SourceRootIngest } import v2.extdeps.languages.c { c_target_model } import v2.extdeps.languages.rust { rust_target_model } -import v2.extdeps.languages.swift { swift_target_model } import v2.std.compilers.target_model { TargetModel } import v2.std.algebra { fold_list, length, list_reverse } @@ -131,18 +130,25 @@ data compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver // to the emitter, and a misspelt one is refused where it was typed instead of reaching the emitter // as a name it has no model for. Adding a target is one arm, one spelling row and one model arm; // the exhaustive matches below refuse to compile until all three exist. +// +// SWIFT IS NOT ROSTERED YET, AND THE REASON IS MEASURED. A rostered target's model is in this +// module's closure, so the seed must emit it to Rust for //gunbc/instruments:v2-native-cli to build +// this door. `v2.extdeps.languages.swift` does not emit cleanly today: the first build carrying it +// failed rustc with E0308 at `swift_bool_grounding` (declared `BooleanAlgebra`, emitted as +// `BooleanAlgebra` while its initializer returns the v2 `Bool` carrier) plus two further +// errors the harness's stderr tail truncated (CI run 36078559948). The trigger that adds the +// `CliTargetSwift` arm is that module becoming seed-emittable, SUFFICIENT FOR the v2-native-cli +// instrument to build this closure with Swift in it. type CliEmitTarget = CliTargetRust | CliTargetC - | CliTargetSwift -data cli_emit_targets: List = [CliTargetRust, CliTargetC, CliTargetSwift] +data cli_emit_targets: List = [CliTargetRust, CliTargetC] fn cli_emit_target_spelling(t: CliEmitTarget) -> String { match t { CliTargetRust => "rust" CliTargetC => "c" - CliTargetSwift => "swift" } } @@ -150,7 +156,6 @@ fn cli_emit_target_model(t: CliEmitTarget) -> TargetModel { match t { CliTargetRust => rust_target_model() CliTargetC => c_target_model() - CliTargetSwift => swift_target_model() } } diff --git a/src/v2/test/claim/cli/v2_native_cli_test.dag b/src/v2/test/claim/cli/v2_native_cli_test.dag index ce30eed74cc..f2a02c007a3 100644 --- a/src/v2/test/claim/cli/v2_native_cli_test.dag +++ b/src/v2/test/claim/cli/v2_native_cli_test.dag @@ -15,7 +15,6 @@ import v2.cli.compile_cli { CliEmitTarget, CliTargetC, CliTargetRust, - CliTargetSwift, CliUsageRefused, cli_emit_target_spelling, v2_cli_exit, @@ -238,7 +237,8 @@ fn argv_emit_with_target(target: String) -> FreeMonoid { } } -data argv_emit_unknown_target: FreeMonoid = argv_emit_with_target(target: "kotlin") +// `swift` is the discriminating unknown: a real target with a model, not yet rostered in this door. +data argv_emit_unknown_target: FreeMonoid = argv_emit_with_target(target: "swift") data argv_emit_dangling_target: FreeMonoid = Cons { head: "emit", @@ -269,10 +269,6 @@ test fn the_c_target_spelling_selects_c_holds() -> Bool { plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "c")), expected: CliTargetC) } -test fn the_swift_target_spelling_selects_swift_holds() -> Bool { - plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "swift")), expected: CliTargetSwift) -} - // NO --target IS THE PLAN'S DECLARED DEFAULT, which is the argv every existing caller spawns. test fn an_emit_without_a_target_plans_the_declared_default_holds() -> Bool { plan_is_target(plan: v2_cli_parse(argv: argv_of(items: argv_emit_one_root)), expected: CliTargetRust) From 3b0f85916f42cee0721c69821e6b6d9f48be549b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 01:46:12 +0000 Subject: [PATCH 4/5] Move the RFM row to its own PR (#12268) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ceeds_the_default_remote_runner_memory.dag | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag diff --git a/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag b/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag deleted file mode 100644 index d95b57511ff..00000000000 --- a/dag/gunbc/recurring_failure_mode/v2_native_route_exceeds_the_default_remote_runner_memory.dag +++ /dev/null @@ -1,26 +0,0 @@ -module gunbc.recurring_failure_mode.v2_native_route_exceeds_the_default_remote_runner_memory - -import std.types { NonEmptyStr } -import std.decl_ref { DeclarationRef, WholeDeclaration } -import gunbc.recurring_failure_mode { RecurringFailureMode } - -data v2_native_route_exceeds_the_default_remote_runner_memory: RecurringFailureMode = RecurringFailureMode { - identity: "v2_native_route_exceeds_the_default_remote_runner_memory" as NonEmptyStr, - - receipts: [ - "neither route that runs the v2 emit pipeline over a real dag/ closure fits the default remote build runner's memory. The two routes are the seed interpreter evaluating v2.compiler.self_host.compiler_closure_emit, and the seed emitting and building v2.cli.compile_cli for //gunbc/instruments:v2-native-cli (INVALID STATE: the instrument that establishes the v2-exclusive door, and the only v2 route that emits a named module's closure, cannot execute on the executor sessions are directed to use. HARM: a session that asks the v2 route what it refuses for a module gets an OOM kill with no stage marker instead of a located diagnostic, so the v2 emitter's refusals for real modules stay unmeasured, and a v2 change that breaks the door is caught only on a larger host).", - - "RECEIPT, measured 2026-09-24/25 while sizing the approve-ios Swift target (dashboard node adhoc-1a935b07-173). Both runs were on the BuildBuddy linux/amd64 runner (7 GB RAM, 6 CPUs, no cgroup memory limit, so the seed refuses to start with HostBudgetUnreadable until the shell is placed in a cgroup; the probe capped memory.max at 6.8 GB). (1) Interpreter route: a probe calling emit_compiler_import_closure_from_ingest_located over the 90-file import closure of gunbc.auth.approval_device_wire was OOM-killed (137) for the rust, c and swift target models alike, after 322 s, 1174 s and 361 s. (2) Built-CLI route: gunbc test //gunbc/instruments:v2-native-cli was OOM-killed (137) after 748 s, while the seed was emitting src/v2/cli/compile_cli.dag in-process, right after compile.normalize and before any crate was assembled (BuildBuddy invocation 40ebdf1e-9699-43e2-9975-83a1f0cad1a6). Neither kill left a stage marker, and memory.peak read back empty.", - - "RECEIPT, same executor, 2026-09-25: the limit is not specific to the two routes above. `gunbc run --claim-run` of single v2 claim files was OOM-killed as well -- src/v2/test/claim/cli/v2_native_cli_test.dag (oom_kill 4), and in one dispatch both a new Swift routing claim and the UNCHANGED src/v2/test/claim/emit/c_semantic_decl_routing_test.dag from main (oom_kill 5). So on this runner, no v2 claim a session changes can be executed before push. The required floor on a larger host is the first place those claims run.", - - "WHY THIS IS A FAILURE MODE AND NOT A SIZING NOTE. An executor too small for the instrument does not produce a typed refusal. It produces a SIGKILL that looks the same as a hang or a crash, and every reader has to rediscover it at the cost of an hour of remote time per attempt. The honest arms are to name the executor class the instrument requires and refuse on a smaller one, or to shrink the route's working set. Retrying on the same runner answers nothing.", - - "RUNG FOUND AT: silent -- nothing declares the memory an instrument needs, and nothing refuses to dispatch it to an executor that lacks it. ATTAINABLE CEILING: mechanically preventable -- an instrument row can declare its required executor memory, and dispatch can refuse an executor below it with a typed cause. NEXT-RUNG TRIGGER, stated as the capability: gunbc.instrument_targets rows carry a declared executor memory floor that the remote dispatch reads and refuses against, SUFFICIENT FOR an under-sized dispatch of //gunbc/instruments:v2-native-cli to refuse before building instead of being OOM-killed. Routing is with the parent lane (eager-owl-205).", - ], - - evidence: [ - DeclarationRef { module_path: "gunbc.instrument_targets", decl_name: "v2_native_cli_label", field: WholeDeclaration }, - DeclarationRef { module_path: "v2.compiler.self_host.compiler_closure_emit", decl_name: "emit_compiler_import_closure_from_ingest_located", field: WholeDeclaration }, - ], -} From 12c70113f3d39a6d7e438b876a57a2ad8b243409 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 07:59:25 +0000 Subject: [PATCH 5/5] v2 CLI: roster Swift now that its target module is seed-emittable (#12270) The trigger stated beside CliEmitTarget is met: #12270 made v2.extdeps.languages.swift and .c emit to Rust cleanly, so the door's closure builds with both rostered. The unknown-target claim's discriminating input moves to 'kotlin', a real target with a model that is not rostered. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/cli/compile_cli.dag | 15 +++++---------- src/v2/test/claim/cli/v2_native_cli_test.dag | 9 +++++++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/v2/cli/compile_cli.dag b/src/v2/cli/compile_cli.dag index 31f81175c82..7ee54a5737d 100644 --- a/src/v2/cli/compile_cli.dag +++ b/src/v2/cli/compile_cli.dag @@ -11,6 +11,7 @@ import v2.compiler.self_host.compiler_closure_emit { import v2.compiler.source_authority { SourceRootIngest } import v2.extdeps.languages.c { c_target_model } import v2.extdeps.languages.rust { rust_target_model } +import v2.extdeps.languages.swift { swift_target_model } import v2.std.compilers.target_model { TargetModel } import v2.std.algebra { fold_list, length, list_reverse } @@ -130,25 +131,18 @@ data compiler_pipeline_entry: CompilerEntryDriver = NativeCliDriver // to the emitter, and a misspelt one is refused where it was typed instead of reaching the emitter // as a name it has no model for. Adding a target is one arm, one spelling row and one model arm; // the exhaustive matches below refuse to compile until all three exist. -// -// SWIFT IS NOT ROSTERED YET, AND THE REASON IS MEASURED. A rostered target's model is in this -// module's closure, so the seed must emit it to Rust for //gunbc/instruments:v2-native-cli to build -// this door. `v2.extdeps.languages.swift` does not emit cleanly today: the first build carrying it -// failed rustc with E0308 at `swift_bool_grounding` (declared `BooleanAlgebra`, emitted as -// `BooleanAlgebra` while its initializer returns the v2 `Bool` carrier) plus two further -// errors the harness's stderr tail truncated (CI run 36078559948). The trigger that adds the -// `CliTargetSwift` arm is that module becoming seed-emittable, SUFFICIENT FOR the v2-native-cli -// instrument to build this closure with Swift in it. type CliEmitTarget = CliTargetRust | CliTargetC + | CliTargetSwift -data cli_emit_targets: List = [CliTargetRust, CliTargetC] +data cli_emit_targets: List = [CliTargetRust, CliTargetC, CliTargetSwift] fn cli_emit_target_spelling(t: CliEmitTarget) -> String { match t { CliTargetRust => "rust" CliTargetC => "c" + CliTargetSwift => "swift" } } @@ -156,6 +150,7 @@ fn cli_emit_target_model(t: CliEmitTarget) -> TargetModel { match t { CliTargetRust => rust_target_model() CliTargetC => c_target_model() + CliTargetSwift => swift_target_model() } } diff --git a/src/v2/test/claim/cli/v2_native_cli_test.dag b/src/v2/test/claim/cli/v2_native_cli_test.dag index f2a02c007a3..208e9253c06 100644 --- a/src/v2/test/claim/cli/v2_native_cli_test.dag +++ b/src/v2/test/claim/cli/v2_native_cli_test.dag @@ -15,6 +15,7 @@ import v2.cli.compile_cli { CliEmitTarget, CliTargetC, CliTargetRust, + CliTargetSwift, CliUsageRefused, cli_emit_target_spelling, v2_cli_exit, @@ -237,8 +238,8 @@ fn argv_emit_with_target(target: String) -> FreeMonoid { } } -// `swift` is the discriminating unknown: a real target with a model, not yet rostered in this door. -data argv_emit_unknown_target: FreeMonoid = argv_emit_with_target(target: "swift") +// `kotlin` is the discriminating unknown: a real target with a model, not rostered in this door. +data argv_emit_unknown_target: FreeMonoid = argv_emit_with_target(target: "kotlin") data argv_emit_dangling_target: FreeMonoid = Cons { head: "emit", @@ -269,6 +270,10 @@ test fn the_c_target_spelling_selects_c_holds() -> Bool { plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "c")), expected: CliTargetC) } +test fn the_swift_target_spelling_selects_swift_holds() -> Bool { + plan_is_target(plan: v2_cli_parse(argv: argv_emit_with_target(target: "swift")), expected: CliTargetSwift) +} + // NO --target IS THE PLAN'S DECLARED DEFAULT, which is the argv every existing caller spawns. test fn an_emit_without_a_target_plans_the_declared_default_holds() -> Bool { plan_is_target(plan: v2_cli_parse(argv: argv_of(items: argv_emit_one_root)), expected: CliTargetRust)