Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/v1/stage0/src/cli_run/native_lane_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,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: <reason>`; for an emit refusal that reason is
/// `v2.cli.compile_cli`'s `the closure did not emit; diagnostic chain: <links> | FATAL AT <locus>`.
Expand All @@ -162,7 +169,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: <binary> emit --entry <module.path> --source-root <dir> [--source-root <dir>]...";
"usage: <binary> emit --entry <module.path> --source-root <dir> [--source-root <dir>]... [--target <target>]";
const CLI_EMIT_REFUSAL_HEAD: &str = "the closure did not emit; diagnostic chain: ";
const CLI_FATAL_AT_MARKER: &str = " | FATAL AT ";

Expand Down Expand Up @@ -1830,16 +1837,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."
))
}
Expand Down
162 changes: 122 additions & 40 deletions src/v2/cli/compile_cli.dag
Original file line number Diff line number Diff line change
@@ -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 }
Expand All @@ -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.
Expand Down Expand Up @@ -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<CliEmitTarget> = [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<CliEmitTarget> {
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<String> }
= CliEmitClosure { entry: String, source_roots: FreeMonoid<String>, 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
Expand All @@ -140,101 +198,118 @@ type CliPlan
// cannot un-refuse it, so the FIRST cause is the one reported.
type CliParseState
= CliParseSeekVerb
| CliParseSeekOption { entry: String, roots: FreeMonoid<String> }
| CliParseSeekRootValue { entry: String, roots: FreeMonoid<String> }
| CliParseSeekEntryValue { entry: String, roots: FreeMonoid<String> }
| CliParseSeekOption { entry: String, roots: FreeMonoid<String>, target: CliEmitTarget }
| CliParseSeekRootValue { entry: String, roots: FreeMonoid<String>, target: CliEmitTarget }
| CliParseSeekEntryValue { entry: String, roots: FreeMonoid<String>, target: CliEmitTarget }
| CliParseSeekTargetValue { entry: String, roots: FreeMonoid<String> }
| 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: <binary> emit --entry <module.path> --source-root <dir> [--source-root <dir>]..."
data cli_usage: String = "usage: <binary> emit --entry <module.path> --source-root <dir> [--source-root <dir>]... [--target <target>]"

fn cli_parse_step(state: CliParseState, arg: String) -> CliParseState {
match state {
CliParseFailed { reason: reason, detail: detail } =>
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,
Expand All @@ -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 }
}
}
}
Expand All @@ -270,14 +345,15 @@ fn v2_cli_plan_source_roots(plan: CliPlan) -> FreeMonoid<String> {
CliPlanRefused { reason: _, detail: _ } => Empty
CliPlanParsed { verb: verb } =>
match verb {
CliEmitClosure { entry: _, source_roots: roots } => roots
CliEmitClosure { entry: _, source_roots: roots, target: _ } => roots
}
}
}

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
Expand Down Expand Up @@ -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,
Expand All @@ -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: _ } =>
Expand Down Expand Up @@ -380,16 +456,22 @@ 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 }
}
}

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: _ } => ""
}
}
Loading