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
42 changes: 24 additions & 18 deletions src/v2/stage0/src/bin/claim_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use std::thread;
use v2_compiler::cli_run::{
make_eval_context, resolve_entry_graph, run_claim, run_value, ClaimOutcome,
};
use v2_compiler::v2_interpreter::Value;
use v2_compiler::v2_interpreter::{InterpContext, Value};

/// One runnable claim, projected from a `ClaimRef` record in the plan value.
#[derive(Clone)]
Expand All @@ -57,7 +57,7 @@ fn require_value(args: &[String], idx: usize, flag: &str) -> Result<String, Exit
/// Walk the std `List` representation (the `FreeMonoid` Cons/Empty coproduct)
/// into a borrowed Vec of element values. `gunbc` renders `List<T>` as
/// `Cons { head, tail } | Empty`, not `Value::List`.
fn free_monoid_elems(value: &Value) -> Result<Vec<&Value>, String> {
fn free_monoid_elems<'a>(value: &'a Value, ctx: &InterpContext) -> Result<Vec<&'a Value>, String> {
let mut out = Vec::new();
let mut cur = value;
loop {
Expand All @@ -66,16 +66,18 @@ fn free_monoid_elems(value: &Value) -> Result<Vec<&Value>, String> {
variant_name,
fields,
..
} if variant_name == "Cons" => {
let head = fields
.get("head")
} if ctx.sym_eq(*variant_name, "Cons") => {
let head = ctx
.field(fields, "head")
.ok_or_else(|| "Cons without `head` field".to_string())?;
out.push(head);
cur = fields
.get("tail")
cur = ctx
.field(fields, "tail")
.ok_or_else(|| "Cons without `tail` field".to_string())?;
}
Value::Variant { variant_name, .. } if variant_name == "Empty" => return Ok(out),
Value::Variant { variant_name, .. } if ctx.sym_eq(*variant_name, "Empty") => {
return Ok(out);
}
// Tolerate an eager `Value::List` too, in case the representation
// ever changes; keeps the walker honest rather than silently wrong.
Value::List(items) => {
Expand All @@ -92,20 +94,24 @@ fn free_monoid_elems(value: &Value) -> Result<Vec<&Value>, String> {
}
}

fn claim_ref_from_value(value: &Value) -> Result<ClaimRef, String> {
fn claim_ref_from_value(value: &Value, ctx: &InterpContext) -> Result<ClaimRef, String> {
let fields = match value {
Value::Record { type_name, fields } if type_name == "ClaimRef" => fields,
Value::Record { type_name, fields } if ctx.sym_eq(*type_name, "ClaimRef") => fields,
other => {
return Err(format!(
"expected a ClaimRef record, got {}",
other.type_label_public()
ctx.format_value(other)
))
}
};
let str_field = |name: &str| -> Result<String, String> {
match fields.get(name) {
match ctx.field(fields, name) {
Some(Value::Str(s)) => Ok(s.clone()),
Some(other) => Err(format!("ClaimRef.{} is {}, not String", name, other)),
Some(other) => Err(format!(
"ClaimRef.{} is {}, not String",
name,
ctx.format_value(other)
)),
None => Err(format!("ClaimRef missing field `{}`", name)),
}
};
Expand All @@ -116,12 +122,12 @@ fn claim_ref_from_value(value: &Value) -> Result<ClaimRef, String> {
}

/// Parse the plan value `List<List<ClaimRef>>` into ordered batches of claims.
fn batches_from_plan(plan: &Value) -> Result<Vec<Vec<ClaimRef>>, String> {
fn batches_from_plan(plan: &Value, ctx: &InterpContext) -> Result<Vec<Vec<ClaimRef>>, String> {
let mut batches = Vec::new();
for batch_val in free_monoid_elems(plan)? {
for batch_val in free_monoid_elems(plan, ctx)? {
let mut batch = Vec::new();
for claim_val in free_monoid_elems(batch_val)? {
batch.push(claim_ref_from_value(claim_val)?);
for claim_val in free_monoid_elems(batch_val, ctx)? {
batch.push(claim_ref_from_value(claim_val, ctx)?);
}
batches.push(batch);
}
Expand Down Expand Up @@ -252,7 +258,7 @@ fn run() -> Result<ExitCode, ExitCode> {
// Drop the plan graph before running claims (keeps no `!Send` state alive).
drop(plan_graph);

let batches = match batches_from_plan(&plan_value) {
let batches = match batches_from_plan(&plan_value, &plan_ctx) {
Ok(b) => b,
Err(msg) => {
eprintln!("claim_executor: malformed plan value: {}", msg);
Expand Down
124 changes: 62 additions & 62 deletions src/v2/stage0/src/cli_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ pub fn run_claim(ctx: &v2_interpreter::InterpContext, function: &str) -> ClaimOu
Ok(v2_interpreter::Value::Bool(true)) => ClaimOutcome::Pass,
Ok(v2_interpreter::Value::Bool(false)) => ClaimOutcome::Fail,
Ok(other) => ClaimOutcome::NotBool {
got: format!("{}", other),
got: ctx.format_value(&other),
},
Err(e) => ClaimOutcome::RuntimeError {
message: format!("{}", e),
Expand Down Expand Up @@ -671,66 +671,64 @@ pub fn handle_run_with_options(
}
};

// Run the interpreter
// Run the interpreter — keep one context alive for symbol resolution while
// printing and classifying the return value (ctrl#1533 phase 3).
eprintln!("running {}()...", function);
match v2_interpreter::run_with_options(
graph,
result.source_indices.clone(),
&function,
dry_run,
!claim_run,
) {
Ok(val) => {
println!("{}", val);
if claim_run {
// Witness entry points return Bool; fail-closed like ProcessExit below.
match &val {
v2_interpreter::Value::Bool(false) => std::process::exit(1),
v2_interpreter::Value::Bool(true) => return,
other => {
let ctx = v2_interpreter::InterpContext::new(graph, result.source_indices.clone(), dry_run);
v2_interpreter::with_active_context(&ctx, || {
match v2_interpreter::run_in_context(&ctx, &function, !claim_run) {
Ok(val) => {
println!("{}", val);
if claim_run {
// Witness entry points return Bool; fail-closed like ProcessExit below.
match &val {
v2_interpreter::Value::Bool(false) => std::process::exit(1),
v2_interpreter::Value::Bool(true) => return,
other => {
eprintln!(
"error: function `{}` returned `{}`, not `Bool`. \
With --claim-run the entry must return Bool (false → exit 1).",
function, other
);
std::process::exit(2);
}
}
}
// FAIL-CLOSED EXIT CODE CONTRACT
//
// Functions invoked via `dag run` MUST return std/process.dag's
// ProcessExit variant. The host translates ExitSuccess → 0 and
// ExitFailure { code } → code. Any other return value is a
// programmer error: the host cannot tell whether the function
// succeeded or failed, so it exits 2 with a clear diagnostic.
//
// This makes silent failure IMPOSSIBLE: a function whose result
// type isn't structurally ProcessExit cannot accidentally exit 0
// when its rich result represents failure. Compose internal
// helpers (check_l1_ratchet → L1RatchetResult) freely; entry
// points must wrap their result in ProcessExit explicitly.
match classify_exit(&val, &ctx) {
ExitClass::Success => {} // exit 0 (default)
ExitClass::Failure(code) => std::process::exit(code),
ExitClass::NotProcessExit { type_name } => {
eprintln!(
"error: function `{}` returned `{}`, not `Bool`. \
With --claim-run the entry must return Bool (false → exit 1).",
function, other
"error: function `{}` returned `{}`, not `ProcessExit`. \
Functions invoked via `dag run` must return std/process.dag's \
ProcessExit so the host can map success/failure to an exit code. \
Wrap your rich result type in ExitSuccess / ExitFailure, or pass \
--claim-run for Bool witness entry points under src/v4.",
function, type_name
);
std::process::exit(2);
}
}
}
// FAIL-CLOSED EXIT CODE CONTRACT
//
// Functions invoked via `dag run` MUST return std/process.dag's
// ProcessExit variant. The host translates ExitSuccess → 0 and
// ExitFailure { code } → code. Any other return value is a
// programmer error: the host cannot tell whether the function
// succeeded or failed, so it exits 2 with a clear diagnostic.
//
// This makes silent failure IMPOSSIBLE: a function whose result
// type isn't structurally ProcessExit cannot accidentally exit 0
// when its rich result represents failure. Compose internal
// helpers (check_l1_ratchet → L1RatchetResult) freely; entry
// points must wrap their result in ProcessExit explicitly.
match classify_exit(&val) {
ExitClass::Success => {} // exit 0 (default)
ExitClass::Failure(code) => std::process::exit(code),
ExitClass::NotProcessExit { type_name } => {
eprintln!(
"error: function `{}` returned `{}`, not `ProcessExit`. \
Functions invoked via `dag run` must return std/process.dag's \
ProcessExit so the host can map success/failure to an exit code. \
Wrap your rich result type in ExitSuccess / ExitFailure, or pass \
--claim-run for Bool witness entry points under src/v4.",
function, type_name
);
std::process::exit(2);
}
Err(e) => {
eprintln!("runtime error: {}", e);
std::process::exit(1);
}
}
Err(e) => {
eprintln!("runtime error: {}", e);
std::process::exit(1);
}
}
});
}

/// Classification of a `dag run` return value for exit-code mapping.
Expand All @@ -751,27 +749,29 @@ enum ExitClass {
/// ProcessExit::ExitSuccess → Success
/// ProcessExit::ExitFailure { code, .. } → Failure(code)
/// anything else → NotProcessExit (fail-closed at host)
fn classify_exit(val: &v2_interpreter::Value) -> ExitClass {
fn classify_exit(val: &v2_interpreter::Value, ctx: &v2_interpreter::InterpContext) -> ExitClass {
match val {
v2_interpreter::Value::Variant {
type_name,
variant_name,
fields,
} => {
if type_name != "ProcessExit" {
if !ctx.sym_eq(*type_name, "ProcessExit") {
return ExitClass::NotProcessExit {
type_name: type_name.clone(),
type_name: ctx.resolve(*type_name),
};
}
match variant_name.as_str() {
"ExitSuccess" => ExitClass::Success,
"ExitFailure" => match fields.get("code") {
if ctx.sym_eq(*variant_name, "ExitSuccess") {
ExitClass::Success
} else if ctx.sym_eq(*variant_name, "ExitFailure") {
match ctx.field(fields, "code") {
Some(v2_interpreter::Value::Int(n)) => ExitClass::Failure(*n as i32),
_ => ExitClass::Failure(1),
},
_ => ExitClass::NotProcessExit {
type_name: format!("ProcessExit::{}", variant_name),
},
}
} else {
ExitClass::NotProcessExit {
type_name: format!("ProcessExit::{}", ctx.resolve(*variant_name)),
}
}
}
_ => ExitClass::NotProcessExit {
Expand Down
Loading
Loading