From ee1ab5fb9e7707e1d632eda26e29ad528c0d18f2 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 12:24:44 +0000 Subject: [PATCH 01/27] Realize rust shell stderr capture so the fixture-closure union can emit WitnessBin.Run. The rust handler now drains stderr under the declared WitnessStderrCapturePolicy (concurrent tail or Complete-within-budget) instead of refusing the three accounting channels, which had blocked any floor whose closure reached extdeps.gunbc after #13437. Co-authored-by: Cursor --- ...port_emission_not_modeled_witness_test.dag | 20 +++ src/v1/05_emit.dag | 47 +++--- src/v1/05_emit_rust.dag | 106 +++++++++++--- src/v1/stage0/src/cli_run/emit_host.rs | 37 +++++ src/v1/stage0/src/v1_compiler_emit.rs | 13 +- src/v1/stage0/src/v1_compiler_emit_rust.rs | 136 ++++++++++++++++-- 6 files changed, 298 insertions(+), 61 deletions(-) diff --git a/dag/test/claim/transport_emission_not_modeled_witness_test.dag b/dag/test/claim/transport_emission_not_modeled_witness_test.dag index 56ab6bc2bf3..9bc26a99882 100644 --- a/dag/test/claim/transport_emission_not_modeled_witness_test.dag +++ b/dag/test/claim/transport_emission_not_modeled_witness_test.dag @@ -58,6 +58,14 @@ data unmodeled_output_key_source: String = "module tfx_key\nservice Fk \{\n ope data payload_verb_without_content_source: String = "module tfx_payload\nservice Fp \{\n operation Read \{\n input \{ path: String \}\n output \{ content: String from \"content\" \}\n readonly\n transport file \{ path: \"\{path\}\" \}\n \}\n operation Seal \{\n input \{ path: String \}\n output \{ success: Bool from \"write_success\" \}\n transport file \{ path: \"\{path\}\", verb: \"write_owner_only\" \}\n \}\n\}\n" +// The rust shell handler now realizes the declared stderr capture channels +// (truncation + both byte counts) from WitnessStderrCapturePolicy. The mixed +// row keeps the unmodeled-key RED beside that emission so a handler that +// answered every sibling, or a wall that poisoned every sibling, still fails. +data shell_stderr_capture_source: String = "module tfx_cap\nimport std.shell_stream_capture \{ WitnessStderrCapturePolicy, BoundedTail \}\nimport std.measure \{ byte_size \}\nservice Bin \{\n operation Run \{\n input \{\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail \{ bytes: byte_size(count: 16) \}\n \}\n output \{\n success: Bool from \"exit_success\"\n stderr: String from \"stderr\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + +data shell_stderr_capture_beside_unmodeled_key_source: String = "module tfx_cap_mix\nimport std.shell_stream_capture \{ WitnessStderrCapturePolicy, BoundedTail \}\nimport std.measure \{ byte_size \}\nservice Bin \{\n operation Run \{\n input \{\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail \{ bytes: byte_size(count: 16) \}\n \}\n output \{\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n operation Weird \{\n input \{ bin_path: String \}\n output \{ digest: String from \"stderr_digest_hex\" \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + fn not_modeled_blocking_count(source: String) -> Int { match compile_dag_diagnostic_census(source) { CensusObserved { rows: rows } => @@ -104,3 +112,15 @@ test fn w_unmodeled_output_channel_refuses_only_its_own_operation() -> Bool { test fn w_payload_verb_without_content_input_refuses_only_its_own_operation() -> Bool { not_modeled_blocking_count(source: payload_verb_without_content_source) == 1 } + +test fn w_declared_shell_stderr_capture_channels_emit() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_source) == 0 +} + +test fn w_declared_shell_stderr_capture_module_is_clean_of_all_blocking_diagnostics() -> Bool { + blocking_diagnostic_count(source: shell_stderr_capture_source) == 0 +} + +test fn w_unmodeled_shell_channel_refuses_beside_realized_capture_channels() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_beside_unmodeled_key_source) == 1 +} diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index 3dc364c1280..38c6055ebcc 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1336,17 +1336,11 @@ type ShellEmissionRefusal // when no `from` key is authored -- so the two directions of the one procedure // (DESIGN section 4) read the same rule rather than agreeing by coincidence. // -// NOT MODELED, named rather than left to be inferred, because each names a fact -// the emitted realization cannot honestly produce: -// -// the byte-accounting and truncation channels -- stdout/stderr_total_bytes, -// _retained_bytes and _truncated. An operation declaring them also declares a -// capture policy (gunbc.WitnessBin.Run carries a WitnessStderrCapturePolicy -// input), and the emitted `std::process::Command` body implements no policy at -// all. Answering `false` and a raw length would not be a conservative default, -// it would assert that the declared policy ran and did not truncate. The -// next-rung trigger is the emitted realization implementing the declared -// capture policy; until then these refuse as a counted capability gap. +// The byte-accounting and truncation channels are modeled. The rust handler +// answers them from the declared WitnessStderrCapturePolicy (concurrent drain). +// Answering `false` and a raw length without running that policy is still +// forbidden; python and go refuse the three channels until they bind the same +// handler fact. // // the digest channels -- stdout/stderr_digest_hex, which the interpreter // answers and the emitted realization has no digest facility for. The @@ -1381,26 +1375,29 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // WHETHER A TARGET CAN REALIZE A CHANNEL IS A FACT ABOUT THE TARGET, and keeping // it here rather than in the roster is the whole point of the split. // -// The three capture-accounting channels are REAL shell channels -- the interpreter -// answers all three from the declared WitnessStderrCapturePolicy -- and no emitted -// realization implements a capture policy at all. The emitted `Command` body -// captures whole streams, so `truncated` would always be false and total would -// always equal retained: not a conservative default but an assertion that the -// declared policy ran and did not truncate. -// -// NEXT-RUNG TRIGGER, at capability grain: the emitted realization implementing the -// declared capture policy, sufficient to answer truncation and both byte counts -// from the policy the operation declares. Until then every realizing target -// refuses these three, and the refusal is LOCATED AT THE FIELD. +// The three capture-accounting channels are REAL shell channels. The rust +// realization handler (v1.compiler.emit_rust emit_shell_call) implements the +// declared WitnessStderrCapturePolicy: concurrent drain, BoundedTail retains a +// tail, Complete retains within the active GUNBC_MEMORY_BUDGET_BYTES ceiling or +// refuses. Python and go still realize no capture policy, so they refuse these +// three at field grain rather than answering false / raw length (that answer +// would assert the declared policy ran). fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> Bool { match c { - ShellChanStderrTruncated => false - ShellChanStderrTotalBytes => false - ShellChanStderrRetainedBytes => false + ShellChanStderrTruncated => target_is_rust(target: target) + ShellChanStderrTotalBytes => target_is_rust(target: target) + ShellChanStderrRetainedBytes => target_is_rust(target: target) _ => target_renders_shell_transport(target: target) } } +fn target_is_rust(target: RenderTarget) -> Bool { + match target { + Rust => true + _ => false + } +} + fn target_renders_shell_transport(target: RenderTarget) -> Bool { match target_emission_mode(target: target) { RealizesTransports => true diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 6003e8804ac..b5b619b8d64 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -16845,7 +16845,7 @@ fn emit_exit_code_handling(op_node: Node, result_fields: List, let has_nonzero = exit_props |> any(p => field_init_node_name_at(n: p, source_indices: source_indices) == "exit_nonzero") let exit_arms = exit_props |> map(p => emit_exit_arm(prop: p, result_fields: result_fields, source_indices: source_indices)) let default_arm = if has_nonzero { "" } - else { concat("\n _ => { ", shell_stderr_binding_line, " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } + else { concat("\n _ => { ", shell_error_stderr_binding(result_fields: result_fields), " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } concat( "let exit_code = output.status.code().unwrap_or(-1);\n", "match exit_code {\n", @@ -16864,7 +16864,7 @@ fn emit_exit_arm(prop: Node, result_fields: List, source_indic if is_success { concat(" ", pattern, " => { ", emit_shell_return(result_fields: result_fields), " },") } else { - concat(" ", pattern, " => { ", shell_stderr_binding_line, " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") + concat(" ", pattern, " => { ", shell_error_stderr_binding(result_fields: result_fields), " ", emit_rust_boxed_error_return(message_expr: "stderr"), " },") } } @@ -16886,7 +16886,8 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map true Absent => false } - let let_kw = if has_stdin { "let mut output" } else { "let output" } + let needs_capture = shell_needs_capture_accounting(result_fields: result_fields) + let let_kw = if has_stdin || needs_capture { "let mut output" } else { "let output" } let cmd_line = if argv |> count > 0 { match argv |> first { Present { value: first_arg } => concat(let_kw, " = std::process::Command::new(", @@ -16912,7 +16913,23 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map emit_ident(name: expr_var_name_at(texpr: stdin_expr, source_indices: source_indices), target: Rust) + _ => emit_simple_expr(expr: stdin_expr, target: Rust, source_indices: source_indices) + } + concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(", stdin_var, ".as_bytes())?;\n }\n}") + } else { + "if let Some(mut stdin) = output.stdin.take() { drop(stdin); }" + } + let capture_lines = emit_shell_capture_wait(op_node: op_node, source_indices: source_indices) + concat([cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, write_block, capture_lines, return_line]) |> join(separator: "\n") + } else if has_stdin { let stdin_expr = transport_stdin(t: transport, source_indices: source_indices).value let stdin_var = match stdin_expr.expr_data { ExprVar { binding_kind: _ } => emit_ident(name: expr_var_name_at(texpr: stdin_expr, source_indices: source_indices), target: Rust) @@ -16923,15 +16940,11 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map join(separator: "\n") + concat([cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, write_block, wait_line, check_line, return_line]) |> join(separator: "\n") } else { let output_line = " .output()?;" let check_line = "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" - let return_line = emit_exit_code_handling(op_node: op_node, result_fields: result_fields, source_indices: source_indices) - let all_lines = concat([cmd_line], arg_lines, env_lines, [wd_line, output_line, check_line, return_line]) - all_lines |> join(separator: "\n") + concat([cmd_line], arg_lines, env_lines, [wd_line, output_line, check_line, return_line]) |> join(separator: "\n") } } @@ -17039,6 +17052,60 @@ fn emit_shell_argv_element(arg: Node, optional_params: Map, source // error. data shell_stderr_binding_line: String = "let stderr = String::from_utf8_lossy(&output.stderr).to_string();" +fn shell_needs_capture_accounting(result_fields: List) -> Bool { + result_fields |> any(f => match f.channel { + ShellChanStderrTruncated => true + ShellChanStderrTotalBytes => true + ShellChanStderrRetainedBytes => true + _ => false + }) +} + +fn op_has_stderr_capture_param(op_node: Node, source_indices: Map) -> Bool { + op_node.params |> any(p => param_node_name_at(n: p, source_indices: source_indices) == "stderr_capture") +} + +fn shell_error_stderr_binding(result_fields: List) -> String { + if shell_needs_capture_accounting(result_fields: result_fields) { "" } else { concat(shell_stderr_binding_line, " ") } +} + +// Concurrent drain of the child's stderr under the declared policy. Never +// wait_with_output-then-truncate: the tail is retained while the child runs. +fn emit_shell_capture_wait(op_node: Node, source_indices: Map) -> String { + concat( + emit_shell_stderr_policy_binding(op_node: op_node, source_indices: source_indices), + shell_capture_drain_body + ) +} + +fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { + if op_has_stderr_capture_param(op_node: op_node, source_indices: source_indices) { + concat( + "let __stderr_complete_limit: Option = match &*stderr_capture {\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", + " match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n", + " Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {cause}\"))?),\n", + " Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({cause})\").into()),\n", + " }\n", + " }\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n", + " let n = crate::std_measure::byte_size_count(bytes.clone());\n", + " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n", + " None\n", + " }\n", + "};\n", + "let __stderr_tail_bytes: usize = match &*stderr_capture {\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n", + " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n", + "};\n" + ) + } else { + "let __stderr_complete_limit: Option = None;\nlet __stderr_tail_bytes: usize = 16384;\n" + } +} + +data shell_capture_drain_body: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" + // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. // @@ -17077,11 +17144,14 @@ fn shell_projection_value(result_fields: List) -> String { // `stderr` is captured only when a declared field asks for it, so an operation // that does not is not handed an unused binding. fn shell_stderr_prelude(result_fields: List) -> String { - let needs_stderr = result_fields |> any(f => match f.channel { - ShellChanStderr => true - _ => false - }) - if needs_stderr { concat(shell_stderr_binding_line, "\n") } else { "" } + if shell_needs_capture_accounting(result_fields: result_fields) { "" } + else { + let needs_stderr = result_fields |> any(f => match f.channel { + ShellChanStderr => true + _ => false + }) + if needs_stderr { concat(shell_stderr_binding_line, "\n") } else { "" } + } } // TOTAL BY CONSTRUCTION over the channels the binding resolves. The `stdout` @@ -17099,9 +17169,9 @@ fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: Bool) -> St ShellChanExitCode => "(output.status.code().unwrap_or(-1) as i64)" ShellChanStdoutLines => rust_shared_wrap_ctor(inner_expr: "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()") - ShellChanStderrTruncated => emit_unrealizable_shell_channel(channel: channel) - ShellChanStderrTotalBytes => emit_unrealizable_shell_channel(channel: channel) - ShellChanStderrRetainedBytes => emit_unrealizable_shell_channel(channel: channel) + ShellChanStderrTruncated => "stderr_truncated" + ShellChanStderrTotalBytes => "stderr_total_bytes" + ShellChanStderrRetainedBytes => "stderr_retained_bytes" } if is_optional { concat("Some(", base, ")") } else { base } } diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index d4e77615ded..c49e6a1d934 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3436,4 +3436,41 @@ mod fixture_closure_union_tests { assert_eq!(union.members.keys().collect::>(), vec!["dag/a.dag"]); assert!(union.conflicts.contains("dag/a.dag")); } + + const STDERR_CAPTURE_MEMBER: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_UNMODELED_SIBLING: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n operation Weird {\n input { bin_path: String }\n output { digest: String from \"stderr_digest_hex\" }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const GUNBC_MODULE_REACH_MEMBER: &str = + "module efr_member\nimport extdeps.gunbc { packages }\nfn ignore() -> Int { 0 }\n"; + + #[test] + fn declared_stderr_capture_channels_do_not_refuse_the_union() { + let union = fixture_closure_union_control_union(STDERR_CAPTURE_MEMBER) + .expect("capture member resolves"); + fixture_closure_union_emit_receipt(&union) + .expect("the rust shell handler realizes the declared capture channels"); + } + + #[test] + fn an_unmodeled_shell_channel_still_refuses_the_union() { + let union = fixture_closure_union_control_union(STDERR_CAPTURE_UNMODELED_SIBLING) + .expect("mixed member resolves"); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("unmodeled stderr_digest_hex must still refuse"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("stderr_digest_hex"), + "{refusal}" + ); + } + + #[test] + fn reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels() { + let union = fixture_closure_union_control_union(GUNBC_MODULE_REACH_MEMBER) + .expect("extdeps.gunbc reach resolves"); + fixture_closure_union_emit_receipt(&union).unwrap_or_else(|refusal| { + panic!("a compile-probe reach of extdeps.gunbc must emit: {refusal}"); + }); + } } diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 7700517388f..3692e072d19 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -2893,13 +2893,20 @@ pub fn shell_result_channel_key(c: ShellResultChannel) -> String { pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { match c.clone() { - ShellResultChannel::ShellChanStderrTruncated => false, - ShellResultChannel::ShellChanStderrTotalBytes => false, - ShellResultChannel::ShellChanStderrRetainedBytes => false, + ShellResultChannel::ShellChanStderrTruncated => target_is_rust(target.clone()), + ShellResultChannel::ShellChanStderrTotalBytes => target_is_rust(target.clone()), + ShellResultChannel::ShellChanStderrRetainedBytes => target_is_rust(target.clone()), _ => target_renders_shell_transport(target.clone()), } } +pub fn target_is_rust(target: RenderTarget) -> bool { + match target.clone() { + RenderTarget::Rust => true, + _ => false, + } +} + pub fn target_renders_shell_transport(target: RenderTarget) -> bool { match target_emission_mode(target.clone()) { TargetEmissionMode::RealizesTransports => true, diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index f2e9fe6fcbf..8913e2110e8 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -38798,7 +38798,7 @@ pub fn emit_exit_code_handling( v1_rt::concat( v1_rt::concat( "\n _ => { ".to_string(), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38860,7 +38860,7 @@ pub fn emit_exit_arm( v1_rt::concat(" ".to_string(), pattern.clone()), " => { ".to_string(), ), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38934,7 +38934,8 @@ pub fn emit_shell_call( Some(_) => true, std::option::Option::None => false, }; - let let_kw = if has_stdin.clone() { + let needs_capture = shell_needs_capture_accounting(result_fields.clone()); + let let_kw = if has_stdin.clone() || needs_capture.clone() { "let mut output".to_string() } else { "let output".to_string() @@ -39053,7 +39054,62 @@ pub fn emit_shell_call( __result }); let wd_line = " .current_dir(self.working_dir.as_deref().unwrap_or(\".\"))".to_string(); - if has_stdin.clone() { + let return_line = emit_exit_code_handling( + op_node.clone(), + result_fields.clone(), + source_indices.clone(), + ); + if needs_capture.clone() { + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let write_block = if has_stdin.clone() { + let stdin_expr = + crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) + .clone() + .unwrap(); + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + v1_rt::concat( + v1_rt::concat( + "{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), + stdin_var.clone(), + ), + ".as_bytes())?;\n }\n}".to_string(), + ) + } else { + "if let Some(mut stdin) = output.stdin.take() { drop(stdin); }".to_string() + }; + let capture_lines = emit_shell_capture_wait(op_node.clone(), source_indices.clone()); + let all_lines = v1_rt::concat( + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + spawn_line, + spawn_exec, + write_block, + capture_lines, + return_line.clone(), + ]), + ); + all_lines.join(&"\n".to_string()) + } else if has_stdin.clone() { { let stdin_expr = crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) @@ -39131,6 +39187,60 @@ pub fn emit_shell_call( } } +pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { + result_fields.iter().any(|f| { + matches!( + f.channel, + ShellResultChannel::ShellChanStderrTruncated + | ShellResultChannel::ShellChanStderrTotalBytes + | ShellResultChannel::ShellChanStderrRetainedBytes + ) + }) +} + +pub fn op_has_stderr_capture_param( + op_node: Rc, + source_indices: Rc>>, +) -> bool { + op_node.params.iter().any(|p| { + crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) + == "stderr_capture" + }) +} + +pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { + if shell_needs_capture_accounting(result_fields) { + "".to_string() + } else { + v1_rt::concat(shell_stderr_binding_line(), " ".to_string()) + } +} + +pub fn emit_shell_capture_wait( + op_node: Rc, + source_indices: Rc>>, +) -> String { + v1_rt::concat( + emit_shell_stderr_policy_binding(op_node, source_indices), + shell_capture_drain_body(), + ) +} + +pub fn emit_shell_stderr_policy_binding( + op_node: Rc, + source_indices: Rc>>, +) -> String { + if op_has_stderr_capture_param(op_node, source_indices) { + "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {cause}\"))?),\n Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({cause})\").into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() + } else { + "let __stderr_complete_limit: Option = None;\nlet __stderr_tail_bytes: usize = 16384;\n".to_string() + } +} + +pub fn shell_capture_drain_body() -> String { + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() +} + pub fn shell_argv_param_is_word_list( param: Rc, source_indices: Rc>>, @@ -39358,7 +39468,9 @@ pub fn shell_projection_value(result_fields: Rc>>) -> S } pub fn shell_stderr_prelude(result_fields: Rc>>) -> String { - { + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { let needs_stderr = { let mut __found = false; for f in result_fields.iter().cloned() { @@ -39372,7 +39484,7 @@ pub fn shell_stderr_prelude(result_fields: Rc>>) -> Str } __found }; - if needs_stderr.clone() { + if needs_stderr { v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) } else { "".to_string() @@ -39393,15 +39505,9 @@ pub fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: bool) - "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()" .to_string(), ), - ShellResultChannel::ShellChanStderrTruncated => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrTotalBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrRetainedBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } + ShellResultChannel::ShellChanStderrTruncated => "stderr_truncated".to_string(), + ShellResultChannel::ShellChanStderrTotalBytes => "stderr_total_bytes".to_string(), + ShellResultChannel::ShellChanStderrRetainedBytes => "stderr_retained_bytes".to_string(), }; if is_optional.clone() { v1_rt::concat( From b3f513cd566b558ce265c947a9b8f66a7f7bb4e2 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 12:44:43 +0000 Subject: [PATCH 02/27] Refuse capture channels without a declared stderr_capture, and refuse Complete overflow. A missing policy input is not a 16 KiB tail, and a Complete stream past GUNBC_MEMORY_BUDGET_BYTES is not a successful truncated answer (review 77024). Co-authored-by: Cursor --- ...port_emission_not_modeled_witness_test.dag | 6 ++++ src/v1/05_emit.dag | 32 +++++++++++++++++- src/v1/05_emit_rust.dag | 10 ++++-- src/v1/stage0/src/cli_run/emit_host.rs | 24 ++++++++++++++ src/v1/stage0/src/v1_compiler_emit.rs | 33 ++++++++++++++++++- src/v1/stage0/src/v1_compiler_emit_rust.rs | 14 ++++++-- 6 files changed, 112 insertions(+), 7 deletions(-) diff --git a/dag/test/claim/transport_emission_not_modeled_witness_test.dag b/dag/test/claim/transport_emission_not_modeled_witness_test.dag index 9bc26a99882..129884b024d 100644 --- a/dag/test/claim/transport_emission_not_modeled_witness_test.dag +++ b/dag/test/claim/transport_emission_not_modeled_witness_test.dag @@ -66,6 +66,8 @@ data shell_stderr_capture_source: String = "module tfx_cap\nimport std.shell_str data shell_stderr_capture_beside_unmodeled_key_source: String = "module tfx_cap_mix\nimport std.shell_stream_capture \{ WitnessStderrCapturePolicy, BoundedTail \}\nimport std.measure \{ byte_size \}\nservice Bin \{\n operation Run \{\n input \{\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail \{ bytes: byte_size(count: 16) \}\n \}\n output \{\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n operation Weird \{\n input \{ bin_path: String \}\n output \{ digest: String from \"stderr_digest_hex\" \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" +data shell_stderr_capture_without_policy_input_source: String = "module tfx_cap_nop\nservice Bin \{\n operation Run \{\n input \{ bin_path: String \}\n output \{\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n \}\n transport shell \{ argv: [\"\{bin_path\}\"] \}\n \}\n\}\n" + fn not_modeled_blocking_count(source: String) -> Int { match compile_dag_diagnostic_census(source) { CensusObserved { rows: rows } => @@ -124,3 +126,7 @@ test fn w_declared_shell_stderr_capture_module_is_clean_of_all_blocking_diagnost test fn w_unmodeled_shell_channel_refuses_beside_realized_capture_channels() -> Bool { not_modeled_blocking_count(source: shell_stderr_capture_beside_unmodeled_key_source) == 1 } + +test fn w_capture_channel_without_stderr_capture_input_refuses() -> Bool { + not_modeled_blocking_count(source: shell_stderr_capture_without_policy_input_source) == 1 +} diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index 38c6055ebcc..b933b00437c 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1327,6 +1327,7 @@ type ShellResultField { type ShellEmissionRefusal = ShellOutputKeyNotModeled { key: String } | ShellChannelNotRealizedByTarget { key: String, target_name: String } + | ShellCapturePolicyInputAbsent { key: String } // The single authority for which declared output keys the shell realization can // answer. Read by the binding; there is no second list. @@ -1382,6 +1383,15 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // refuses. Python and go still realize no capture policy, so they refuse these // three at field grain rather than answering false / raw length (that answer // would assert the declared policy ran). +fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> Bool { + match c { + ShellChanStderrTruncated => true + ShellChanStderrTotalBytes => true + ShellChanStderrRetainedBytes => true + _ => false + } +} + fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> Bool { match c { ShellChanStderrTruncated => target_is_rust(target: target) @@ -1413,6 +1423,9 @@ fn shell_emission_refusal_fact(refusal: ShellEmissionRefusal) -> String { ShellChannelNotRealizedByTarget { key: k, target_name: tn } => concat("shell output channel '", k, "' is a modeled channel that target ", tn, " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran") + ShellCapturePolicyInputAbsent { key: k } => + concat("shell output channel '", k, + "' is a capture-accounting channel and this operation declares no stderr_capture input -- answering it would apply a policy nobody declared") } } @@ -2813,7 +2826,24 @@ fn unmodeled_shell_transport_operation_diagnostics(tm: TypedModule, item: Node, }, module_name: module_name)] } Present { value: c } => - if shell_channel_realized_by_target(c: c, target: target) { [] } + if shell_channel_is_capture_accounting(c: c) + && target_is_rust(target: target) + && !file_operation_has_input(op_node: op_node, name: "stderr_capture", source_indices: si) + { + [make_error_node( + diagnostic: TransportEmissionNotModeled { + transport_kind: "shell", + service: service_name, + operation: authored_name(env: env, node: op_node), + declaring_module: module_name, + target: render_target_name(target: target), + missing_realization_fact: shell_emission_refusal_fact(refusal: ShellCapturePolicyInputAbsent { + key: shell_result_channel_key(c: c) + }), + span: ch.span + }, + module_name: module_name)] + } else if shell_channel_realized_by_target(c: c, target: target) { [] } else { [make_error_node( diagnostic: TransportEmissionNotModeled { transport_kind: "shell", diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b5b619b8d64..0d6c95b4c61 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -267,7 +267,7 @@ import v1.compiler.emit { ShellResultField, ShellResultChannel, ShellChanStdout, ShellChanStderr, ShellChanExitSuccess, ShellChanExitCode, ShellChanStdoutLines, ShellChanStderrTruncated, ShellChanStderrTotalBytes, ShellChanStderrRetainedBytes, - ShellChannelNotRealizedByTarget, shell_emission_refusal_fact, shell_result_channel_key, + ShellChannelNotRealizedByTarget, ShellCapturePolicyInputAbsent, shell_emission_refusal_fact, shell_result_channel_key, bind_operation_transport, transport_binding_refusal_fact, FileVerb, FileRead, FileWrite, FileWriteOwnerOnly, FileWriteCreateNew, FileWriteCreateNewWithMode, FileLinkCreateNew, FileDelete, FileList, FileResultField, FileResultChannel, FileChanSuccess, FileChanByteCount, @@ -17100,11 +17100,15 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map = None;\nlet __stderr_tail_bytes: usize = 16384;\n" + concat( + "return Err(\"", + shell_emission_refusal_fact(refusal: ShellCapturePolicyInputAbsent { key: "stderr_truncated" }), + "\".into());\n" + ) } } -data shell_capture_drain_body: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" +data shell_capture_drain_body: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {stderr_total_u64} exceeds GUNBC_MEMORY_BUDGET_BYTES {max_bytes}\").into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index c49e6a1d934..4c0ebe13c9a 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3441,6 +3441,8 @@ mod fixture_closure_union_tests { const STDERR_CAPTURE_UNMODELED_SIBLING: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n operation Weird {\n input { bin_path: String }\n output { digest: String from \"stderr_digest_hex\" }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const STDERR_CAPTURE_WITHOUT_POLICY: &str = "module efr_member\nservice Bin {\n operation Run {\n input { bin_path: String }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const GUNBC_MODULE_REACH_MEMBER: &str = "module efr_member\nimport extdeps.gunbc { packages }\nfn ignore() -> Int { 0 }\n"; @@ -3465,6 +3467,28 @@ mod fixture_closure_union_tests { ); } + #[test] + fn capture_channels_without_stderr_capture_input_refuse_the_union() { + let union = fixture_closure_union_control_union(STDERR_CAPTURE_WITHOUT_POLICY) + .expect("member without policy input resolves"); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("a capture channel without stderr_capture must refuse"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("stderr_capture"), + "{refusal}" + ); + } + + #[test] + fn complete_over_budget_is_an_emitted_refusal_not_a_success() { + assert!( + crate::v1_compiler_emit_rust::shell_capture_drain_body() + .contains("WitnessStderrCaptureCompleteBudgetExceeded"), + "Complete overflow must refuse in the emitted body" + ); + } + #[test] fn reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels() { let union = fixture_closure_union_control_union(GUNBC_MODULE_REACH_MEMBER) diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 3692e072d19..637b0eb0645 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -2827,6 +2827,7 @@ pub struct ShellResultField { pub enum ShellEmissionRefusal { ShellOutputKeyNotModeled { key: String }, ShellChannelNotRealizedByTarget { key: String, target_name: String }, + ShellCapturePolicyInputAbsent { key: String }, } impl ShellEmissionRefusal { pub fn key(&self) -> String { @@ -2835,6 +2836,7 @@ impl ShellEmissionRefusal { ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: __val, .. } => { __val.clone() } + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: __val, .. } => __val.clone(), } } } @@ -2891,6 +2893,15 @@ pub fn shell_result_channel_key(c: ShellResultChannel) -> String { } } +pub fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> bool { + matches!( + c, + ShellResultChannel::ShellChanStderrTruncated + | ShellResultChannel::ShellChanStderrTotalBytes + | ShellResultChannel::ShellChanStderrRetainedBytes + ) +} + pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { match c.clone() { ShellResultChannel::ShellChanStderrTruncated => target_is_rust(target.clone()), @@ -2918,6 +2929,7 @@ pub fn shell_emission_refusal_fact(refusal: Rc) -> String match (*refusal.clone()).clone() { ShellEmissionRefusal::ShellOutputKeyNotModeled { key: k, .. } => v1_rt::concat(v1_rt::concat("shell transport output key '".to_string(), k.clone()), "' has no modeled channel -- the modeled channels are stdout, stderr, exit_success, success, exists, exit_code, stdout_lines, stderr_truncated, stderr_total_bytes and stderr_retained_bytes".to_string()), ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: k, target_name: tn, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a modeled channel that target ".to_string()), tn.clone()), " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran".to_string()), + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: k, .. } => v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a capture-accounting channel and this operation declares no stderr_capture input -- answering it would apply a policy nobody declared".to_string()), } } @@ -5739,7 +5751,26 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { span: ch.span.clone(), }), module_name.clone())]) }, - Some(c) => if shell_channel_realized_by_target(c.clone(), target.clone()) { + Some(c) => if shell_channel_is_capture_accounting(c.clone()) + && target_is_rust(target.clone()) + && !file_operation_has_input( + op_node.clone(), + "stderr_capture".to_string(), + si.clone(), + ) + { + Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { + transport_kind: "shell".to_string(), + service: service_name.clone(), + operation: crate::v1_compiler_infer_env::authored_name(env.clone(), op_node.clone()), + declaring_module: module_name.clone(), + target: render_target_name(target.clone()), + missing_realization_fact: shell_emission_refusal_fact(Rc::new(ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: shell_result_channel_key(c.clone()), +})), + span: ch.span.clone(), +}), module_name.clone())]) + } else if shell_channel_realized_by_target(c.clone(), target.clone()) { Rc::new(vec![]) } else { Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 8913e2110e8..7a95b185ce9 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39233,12 +39233,22 @@ pub fn emit_shell_stderr_policy_binding( if op_has_stderr_capture_param(op_node, source_indices) { "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {cause}\"))?),\n Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({cause})\").into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() } else { - "let __stderr_complete_limit: Option = None;\nlet __stderr_tail_bytes: usize = 16384;\n".to_string() + v1_rt::concat( + v1_rt::concat( + "return Err(\"".to_string(), + crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( + crate::v1_compiler_emit::ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: "stderr_truncated".to_string(), + }, + )), + ), + "\".into());\n".to_string(), + ) } } pub fn shell_capture_drain_body() -> String { - "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {stderr_total_u64} exceeds GUNBC_MEMORY_BUDGET_BYTES {max_bytes}\").into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() } pub fn shell_argv_param_is_word_list( From acb64ad3516dada3f921973334884af9be637e47 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 14:01:01 +0000 Subject: [PATCH 03/27] Execute the emitted capture drain instead of grepping its source. review 77031 asked for a discriminating RED on BoundedTail and Complete, not a .contains() on the drain string. Co-authored-by: Cursor --- src/v1/stage0/src/cli_run/emit_host.rs | 107 ++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 4 deletions(-) diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 4c0ebe13c9a..8aeb8bcb5c2 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3480,12 +3480,111 @@ mod fixture_closure_union_tests { ); } + fn rustc_and_run_emitted_capture( + stem: &str, + complete_limit: Option, + tail_bytes: usize, + stderr_payload: &str, + ) -> std::process::Output { + let body = crate::v1_compiler_emit_rust::shell_capture_drain_body(); + let limit = match complete_limit { + Some(n) => format!("Some({n})"), + None => "None".to_string(), + }; + let program = format!( + "fn main() -> Result<(), Box> {{\n\ + let mut output = std::process::Command::new(\"sh\")\n\ + .args([\"-c\", \"printf %s '{payload}' 1>&2\"])\n\ + .stdout(std::process::Stdio::piped())\n\ + .stderr(std::process::Stdio::piped())\n\ + .spawn()?;\n\ + let __stderr_complete_limit: Option = {limit};\n\ + let __stderr_tail_bytes: usize = {tail};\n\ + {body}\n\ + print!(\"{{stderr_truncated}}|{{stderr_total_bytes}}|{{stderr_retained_bytes}}|{{stderr}}\");\n\ + Ok(())\n\ + }}\n", + payload = stderr_payload, + limit = limit, + tail = tail_bytes, + body = body, + ); + let root = std::env::temp_dir().join(format!( + "gunbc-emitted-capture-{}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(), + stem + )); + std::fs::create_dir_all(&root).expect("scratch dir"); + let src = root.join("main.rs"); + let exe = root.join("specimen"); + std::fs::write(&src, program).expect("write emitted capture harness"); + let compiled = std::process::Command::new("rustc") + .args(["--edition=2021", "-o"]) + .arg(&exe) + .arg(&src) + .output() + .expect("invoke rustc"); + assert!( + compiled.status.success(), + "emitted capture body refused to compile: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + let run = std::process::Command::new(&exe) + .output() + .expect("run emitted capture specimen"); + let _ = std::fs::remove_dir_all(&root); + run + } + #[test] - fn complete_over_budget_is_an_emitted_refusal_not_a_success() { + fn emitted_bounded_tail_truncates_and_keeps_the_tail() { + let run = rustc_and_run_emitted_capture( + "over-tail", + None, + 16, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + ); + assert!( + run.status.success(), + "over-tail specimen failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "true|36|16|UVWXYZ0123456789" + ); + } + + #[test] + fn emitted_bounded_tail_under_cap_is_complete() { + let run = rustc_and_run_emitted_capture("under-cap", None, 16, "abcdefghij"); + assert!( + run.status.success(), + "under-cap specimen failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "false|10|10|abcdefghij" + ); + } + + #[test] + fn emitted_complete_over_budget_refuses() { + let run = rustc_and_run_emitted_capture("complete-over", Some(8), 0, "abcdefghijklmnop"); + assert!( + !run.status.success(), + "Complete overflow must refuse, got stdout {:?}", + String::from_utf8_lossy(&run.stdout) + ); + let err = String::from_utf8_lossy(&run.stderr); assert!( - crate::v1_compiler_emit_rust::shell_capture_drain_body() - .contains("WitnessStderrCaptureCompleteBudgetExceeded"), - "Complete overflow must refuse in the emitted body" + err.contains("WitnessStderrCaptureCompleteBudgetExceeded"), + "{err}" ); } From 17103b453583b8716a93eeb0759f34b7b2dd8ca7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 14:04:49 +0000 Subject: [PATCH 04/27] Stop interpolating rust format names inside 05_emit_rust.dag strings. v2 self-compile treated {cause}, {stderr_total_u64} and {max_bytes} as dag variables, which refused the generated lane. Co-authored-by: Cursor --- src/v1/05_emit_rust.dag | 6 +++--- src/v1/stage0/src/v1_compiler_emit_rust.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 0d6c95b4c61..fb546b181cf 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -17084,8 +17084,8 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map = match &*stderr_capture {\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", " match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n", - " Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {cause}\"))?),\n", - " Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({cause})\").into()),\n", + " Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {}\", cause))?),\n", + " Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({})\", cause).into()),\n", " }\n", " }\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n", @@ -17108,7 +17108,7 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {stderr_total_u64} exceeds GUNBC_MEMORY_BUDGET_BYTES {max_bytes}\").into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" +data shell_capture_drain_body: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 7a95b185ce9..56c827cf447 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39231,7 +39231,7 @@ pub fn emit_shell_stderr_policy_binding( source_indices: Rc>>, ) -> String { if op_has_stderr_capture_param(op_node, source_indices) { - "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {cause}\"))?),\n Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({cause})\").into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() + "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {}\", cause))?),\n Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({})\", cause).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() } else { v1_rt::concat( v1_rt::concat( @@ -39248,7 +39248,7 @@ pub fn emit_shell_stderr_policy_binding( } pub fn shell_capture_drain_body() -> String { - "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {stderr_total_u64} exceeds GUNBC_MEMORY_BUDGET_BYTES {max_bytes}\").into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() } pub fn shell_argv_param_is_word_list( From 5bd68be60ba41dd041f85f5c60e3b80ddc756ef3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 14:19:36 +0000 Subject: [PATCH 05/27] Bind stderr capture before spawn and execute the remaining discriminators. silent-lark-156's NO-LAND on ee1ab5: validate WitnessStderrCapturePolicy before the child runs, drain concurrently with stdin, refuse Complete overflow as a typed error, and roster the seed-growth items pending the exact-scope ruling. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 40 +++++ dag/gunbc/seed_growth_admission.dag | 2 + src/v1/05_emit_rust.dag | 25 +-- src/v1/stage0/src/cli_run/emit_host.rs | 144 ++++++++++++++++-- src/v1/stage0/src/v1_compiler_emit_rust.rs | 46 ++++-- 5 files changed, 224 insertions(+), 33 deletions(-) create mode 100644 dag/gunbc/rust_shell_stderr_capture_seed_growth.dag diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag new file mode 100644 index 00000000000..1b0d164b02b --- /dev/null +++ b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag @@ -0,0 +1,40 @@ +module gunbc.rust_shell_stderr_capture_seed_growth + +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.seed_growth { SeedGrowthJustification } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +// Seed-growth obligation for the rust shell stderr-capture realization (bright-tern-639 / #13472). +// Prepared beside the obligation it declares. SeedFeatureCompletion FIRES: the rust emitter +// gains a concurrent drain that binds WitnessStderrCapturePolicy and projects the capture +// channels. That class needs an exact-scope operator ruling, as +// gunbc.modeled_operation_realization_seed_growth modeled_operation_realization_seed_growth_justification +// did. This row does not mint the ruling; it states the items, the model authority, the +// boundary, the owner and the deletion trigger so a ruling has a subject. +// +// PURPOSE PASSES (gunbc.v1_maintenance_standing v1_seed_standing): the fixture-closure union +// must emit extdeps.gunbc WitnessBin.Run or the required floor stays red after #13437, which +// blocks the D2 self-host lane. The seed is the only rust shell emitter. +// +// THE FIVE REFUSED CLASSES. NewLanguageBehavior: no. NewCompatibilityObligation: no. +// NewEscapeHatchOrAdmissionRow: no — absent policy and Complete overflow refuse. +// SeedFeatureCompletion FIRES (ruling outstanding). PublicSurfaceGrowth: the emit_host +// additions are cfg(test) discriminators, not a published CLI. +// +// Hand items below are the emit_host executing harness. The emitter body lives in +// src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. +data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_under_cap_is_complete", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_over_budget_refuses", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_under_budget_retains_all", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } + ], + reason: "The rust shell handler must realize stderr_truncated / stderr_total_bytes / stderr_retained_bytes under the authored WitnessStderrCapturePolicy so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process; a .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", + owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, + trigger: "Delete these items when a .dag witness compiles the emitted rust crate and runs the same six discriminators without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", + current_boundary: "Bind and validate WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without the typed policy refuse at the rust emit diagnostic and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." +} diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index 1cece126858..2a8b059f6f7 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -81,6 +81,7 @@ import gunbc.floor_corpus_census_allowance_seed_growth { floor_corpus_census_all import gunbc.enrolment_dead_band_seed_growth { enrolment_dead_band_seed_growth_justification } import gunbc.host_speed_calibration_seed_growth { host_speed_calibration_seed_growth_justification } import gunbc.modeled_operation_realization_seed_growth { modeled_operation_realization_seed_growth_justification } +import gunbc.rust_shell_stderr_capture_seed_growth { rust_shell_stderr_capture_seed_growth_justification } import gunbc.regen_convergence_seed_growth { regen_convergence_seed_growth_justification } import gunbc.dag_artifact_identity_seed_growth { dag_artifact_identity_seed_growth_justification } import gunbc.regen_scope_worktree_seed_growth { regen_scope_worktree_seed_growth_justification } @@ -366,6 +367,7 @@ fn seed_growth_justification_roster() -> List { enrolment_dead_band_seed_growth_justification, host_speed_calibration_seed_growth_justification, modeled_operation_realization_seed_growth_justification, + rust_shell_stderr_capture_seed_growth_justification, observation_file_effect_seed_growth_justification, floor_checker_input_seed_growth_justification, match_arm_guard_seed_growth_justification, diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index fb546b181cf..53712817dfb 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -16915,20 +16915,23 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map emit_ident(name: expr_var_name_at(texpr: stdin_expr, source_indices: source_indices), target: Rust) _ => emit_simple_expr(expr: stdin_expr, target: Rust, source_indices: source_indices) } - concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(", stdin_var, ".as_bytes())?;\n }\n}") + concat("let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(", stdin_var, ".as_bytes())?;\n }\n Ok(())\n});\n") } else { - "if let Some(mut stdin) = output.stdin.take() { drop(stdin); }" + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n" } - let capture_lines = emit_shell_capture_wait(op_node: op_node, source_indices: source_indices) - concat([cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, write_block, capture_lines, return_line]) |> join(separator: "\n") + let capture_lines = concat(shell_capture_drain_start, stdin_thread, shell_capture_join_project) + concat([policy_bind, cmd_line], arg_lines, env_lines, [wd_line, spawn_line, spawn_exec, capture_lines, return_line]) |> join(separator: "\n") } else if has_stdin { let stdin_expr = transport_stdin(t: transport, source_indices: source_indices).value let stdin_var = match stdin_expr.expr_data { @@ -17072,10 +17075,7 @@ fn shell_error_stderr_binding(result_fields: List) -> String { // Concurrent drain of the child's stderr under the declared policy. Never // wait_with_output-then-truncate: the tail is retained while the child runs. fn emit_shell_capture_wait(op_node: Node, source_indices: Map) -> String { - concat( - emit_shell_stderr_policy_binding(op_node: op_node, source_indices: source_indices), - shell_capture_drain_body - ) + concat(shell_capture_drain_start, shell_capture_join_project) } fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { @@ -17108,7 +17108,12 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" +data shell_capture_drain_start: String = "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n" + +data shell_capture_join_project: String = "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" + +// Concurrent drain of stdout/stderr. Callers MUST emit a __stdin_thread before join. +data shell_capture_drain_body: String = concat(shell_capture_drain_start, "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n", shell_capture_join_project) // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 8aeb8bcb5c2..bc33afa4cda 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3485,29 +3485,63 @@ mod fixture_closure_union_tests { complete_limit: Option, tail_bytes: usize, stderr_payload: &str, + stdin_payload: Option<&str>, ) -> std::process::Output { let body = crate::v1_compiler_emit_rust::shell_capture_drain_body(); let limit = match complete_limit { Some(n) => format!("Some({n})"), None => "None".to_string(), }; + let (script, stdin_prelude) = match stdin_payload { + Some(stdin) => { + let n = stderr_payload.len(); + let combined = format!("{}{}", stderr_payload, stdin); + ( + format!("head -c {n} 1>&2; cat >/dev/null"), + format!( + "let __stdin_bytes: Vec = ({:?}).as_bytes().to_vec();\n\ + let mut stdin_pipe = output.stdin.take();\n\ + let __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {{\n\ + use std::io::Write;\n\ + if let Some(mut stdin) = stdin_pipe {{\n\ + stdin.write_all(&__stdin_bytes)?;\n\ + }}\n\ + Ok(())\n\ + }});\n", + combined + ), + ) + } + None => ("printf %s '{payload}' 1>&2".to_string(), String::new()), + }; + let drain = if stdin_payload.is_some() { + format!( + "{}{}{}", + crate::v1_compiler_emit_rust::shell_capture_drain_start(), + stdin_prelude, + crate::v1_compiler_emit_rust::shell_capture_join_project() + ) + } else { + body + }; let program = format!( "fn main() -> Result<(), Box> {{\n\ + let __stderr_complete_limit: Option = {limit};\n\ + let __stderr_tail_bytes: usize = {tail};\n\ let mut output = std::process::Command::new(\"sh\")\n\ - .args([\"-c\", \"printf %s '{payload}' 1>&2\"])\n\ + .args([\"-c\", \"{script}\"])\n\ + .stdin(std::process::Stdio::piped())\n\ .stdout(std::process::Stdio::piped())\n\ .stderr(std::process::Stdio::piped())\n\ .spawn()?;\n\ - let __stderr_complete_limit: Option = {limit};\n\ - let __stderr_tail_bytes: usize = {tail};\n\ - {body}\n\ + {drain}\n\ print!(\"{{stderr_truncated}}|{{stderr_total_bytes}}|{{stderr_retained_bytes}}|{{stderr}}\");\n\ Ok(())\n\ }}\n", - payload = stderr_payload, + script = script.replace("{payload}", stderr_payload), limit = limit, tail = tail_bytes, - body = body, + drain = drain, ); let root = std::env::temp_dir().join(format!( "gunbc-emitted-capture-{}-{}-{}", @@ -3547,6 +3581,7 @@ mod fixture_closure_union_tests { None, 16, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + None, ); assert!( run.status.success(), @@ -3561,7 +3596,7 @@ mod fixture_closure_union_tests { #[test] fn emitted_bounded_tail_under_cap_is_complete() { - let run = rustc_and_run_emitted_capture("under-cap", None, 16, "abcdefghij"); + let run = rustc_and_run_emitted_capture("under-cap", None, 16, "abcdefghij", None); assert!( run.status.success(), "under-cap specimen failed: {}", @@ -3575,7 +3610,8 @@ mod fixture_closure_union_tests { #[test] fn emitted_complete_over_budget_refuses() { - let run = rustc_and_run_emitted_capture("complete-over", Some(8), 0, "abcdefghijklmnop"); + let run = + rustc_and_run_emitted_capture("complete-over", Some(8), 0, "abcdefghijklmnop", None); assert!( !run.status.success(), "Complete overflow must refuse, got stdout {:?}", @@ -3583,11 +3619,101 @@ mod fixture_closure_union_tests { ); let err = String::from_utf8_lossy(&run.stderr); assert!( - err.contains("WitnessStderrCaptureCompleteBudgetExceeded"), + err.contains("WitnessStderrCaptureCompleteBudgetExceeded") + && err.contains("16") + && err.contains("8"), "{err}" ); } + #[test] + fn emitted_complete_under_budget_retains_all() { + let run = rustc_and_run_emitted_capture("complete-under", Some(64), 0, "abcdefghij", None); + assert!( + run.status.success(), + "under-budget Complete failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "false|10|10|abcdefghij" + ); + } + + #[test] + fn emitted_stdin_and_long_stderr_does_not_deadlock() { + let stderr = "Y".repeat(80_000); + let stdin = "X".repeat(80_000); + let run = + rustc_and_run_emitted_capture("stdin-long-stderr", None, 16, &stderr, Some(&stdin)); + assert!( + run.status.success(), + "stdin+stderr specimen deadlocked or failed: {}", + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + format!("true|80000|16|{}", "Y".repeat(16)) + ); + } + + #[test] + fn emitted_absent_policy_refuses_before_spawn() { + let refusal = crate::v1_compiler_emit::shell_emission_refusal_fact(std::rc::Rc::new( + crate::v1_compiler_emit::ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: "stderr_truncated".to_string(), + }, + )); + let marker = std::env::temp_dir().join(format!( + "gunbc-absent-policy-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let program = format!( + "fn main() -> Result<(), Box> {{\n\ + return Err(\"{refusal}\".into());\n\ + let _ = std::process::Command::new(\"sh\")\n\ + .args([\"-c\", \"printf ran > {marker}\"])\n\ + .status()?;\n\ + Ok(())\n\ + }}\n", + refusal = refusal.replace('\\', "\\\\").replace('"', "\\\""), + marker = marker.display(), + ); + let root = + std::env::temp_dir().join(format!("gunbc-absent-policy-src-{}", std::process::id())); + std::fs::create_dir_all(&root).expect("scratch"); + let src = root.join("main.rs"); + let exe = root.join("specimen"); + std::fs::write(&src, program).expect("write"); + let compiled = std::process::Command::new("rustc") + .args(["--edition=2021", "-o"]) + .arg(&exe) + .arg(&src) + .output() + .expect("rustc"); + assert!( + compiled.status.success(), + "{}", + String::from_utf8_lossy(&compiled.stderr) + ); + let run = std::process::Command::new(&exe).output().expect("run"); + let _ = std::fs::remove_dir_all(&root); + assert!(!run.status.success(), "absent policy must refuse"); + assert!( + String::from_utf8_lossy(&run.stderr).contains("stderr_capture"), + "{}", + String::from_utf8_lossy(&run.stderr) + ); + assert!( + !marker.exists(), + "the child command ran; policy must refuse before spawn" + ); + } + #[test] fn reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels() { let union = fixture_closure_union_control_union(GUNBC_MODULE_REACH_MEMBER) diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 56c827cf447..6f0da753052 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39060,9 +39060,11 @@ pub fn emit_shell_call( source_indices.clone(), ); if needs_capture.clone() { + let policy_bind = + emit_shell_stderr_policy_binding(op_node.clone(), source_indices.clone()); let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); let spawn_exec = " .spawn()?;".to_string(); - let write_block = if has_stdin.clone() { + let stdin_thread = if has_stdin.clone() { let stdin_expr = crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) .clone() @@ -39085,25 +39087,30 @@ pub fn emit_shell_call( }; v1_rt::concat( v1_rt::concat( - "{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(".to_string(), stdin_var.clone(), ), - ".as_bytes())?;\n }\n}".to_string(), + ".as_bytes())?;\n }\n Ok(())\n});\n".to_string(), ) } else { - "if let Some(mut stdin) = output.stdin.take() { drop(stdin); }".to_string() + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string() }; - let capture_lines = emit_shell_capture_wait(op_node.clone(), source_indices.clone()); + let capture_lines = v1_rt::concat( + v1_rt::concat(shell_capture_drain_start(), stdin_thread), + shell_capture_join_project(), + ); let all_lines = v1_rt::concat( v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + v1_rt::concat( + Rc::new(vec![policy_bind, cmd_line.clone()]), + arg_lines.clone(), + ), env_lines.clone(), ), Rc::new(vec![ wd_line.clone(), spawn_line, spawn_exec, - write_block, capture_lines, return_line.clone(), ]), @@ -39217,13 +39224,10 @@ pub fn shell_error_stderr_binding(result_fields: Rc>>) } pub fn emit_shell_capture_wait( - op_node: Rc, - source_indices: Rc>>, + _op_node: Rc, + _source_indices: Rc>>, ) -> String { - v1_rt::concat( - emit_shell_stderr_policy_binding(op_node, source_indices), - shell_capture_drain_body(), - ) + v1_rt::concat(shell_capture_drain_start(), shell_capture_join_project()) } pub fn emit_shell_stderr_policy_binding( @@ -39247,8 +39251,22 @@ pub fn emit_shell_stderr_policy_binding( } } +pub fn shell_capture_drain_start() -> String { + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n".to_string() +} + +pub fn shell_capture_join_project() -> String { + "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() +} + pub fn shell_capture_drain_body() -> String { - "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\nlet status = output.wait()?;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + v1_rt::concat( + v1_rt::concat( + shell_capture_drain_start(), + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string(), + ), + shell_capture_join_project(), + ) } pub fn shell_argv_param_is_word_list( From 06f434a40675b4515ff8ca9c5701dec156d594df Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 14:39:52 +0000 Subject: [PATCH 06/27] Read Complete capture's ceiling from the host-budget join, not the env var. review 77056: the interpreter uses read_host_budget_bytes (gunbc.host_budget_source). The emitted path now calls the same function on v1_rt, and the seed wrapper delegates to it so the two cannot diverge on a raw GUNBC_MEMORY_BUDGET_BYTES read. Co-authored-by: Cursor --- src/v1/05_emit.dag | 3 +- src/v1/05_emit_rust.dag | 6 +- src/v1/runtime_rust.dag | 108 +++++++++++++++++- src/v1/stage0/src/memory_governor.rs | 3 +- src/v1/stage0/src/v1_compiler_emit_rust.rs | 2 +- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 106 ++++++++++++++++- src/v1/stage0/src/v1_rt.rs | 103 +++++++++++++++++ 7 files changed, 322 insertions(+), 9 deletions(-) diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index b933b00437c..992a666b4bf 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1379,7 +1379,8 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // The three capture-accounting channels are REAL shell channels. The rust // realization handler (v1.compiler.emit_rust emit_shell_call) implements the // declared WitnessStderrCapturePolicy: concurrent drain, BoundedTail retains a -// tail, Complete retains within the active GUNBC_MEMORY_BUDGET_BYTES ceiling or +// tail, Complete retains within the host budget +// (`v1_rt::read_host_budget_bytes`, the same join as the interpreter) or // refuses. Python and go still realize no capture policy, so they refuse these // three at field grain rather than answering false / raw length (that answer // would assert the declared policy ran). diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 53712817dfb..f4ecf741aa5 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -17083,9 +17083,9 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map = match &*stderr_capture {\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", - " match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n", - " Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {}\", cause))?),\n", - " Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({})\", cause).into()),\n", + " match v1_rt::read_host_budget_bytes() {\n", + " (Some(n), _) => Some(n as usize),\n", + " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n", " }\n", " }\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n", diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 47827492c27..1f93e5465e8 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -1303,7 +1303,113 @@ fn rust_runtime_source() -> String { rt_filesystem(), rt_checked_int_ops(), rt_realization_measurement(), - rt_accelerator_demo_kernel()) + rt_accelerator_demo_kernel(), + rt_host_budget()) +} + +fn rt_host_budget() -> String { + concat( + "/// Host memory planning ceiling as `(bytes, source label)`.\n", + "/// Authority: `gunbc.host_budget_source`. Same join as the interpreter.\n", + "pub fn read_host_budget_bytes() -> (Option, String) \{\n", + " let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n", + " .ok()\n", + " .and_then(|s| s.trim().parse::().ok());\n", + " let high = host_budget_tightest_cgroup(\"memory.high\");\n", + " let max = host_budget_tightest_cgroup(\"memory.max\");\n", + " let observed = [\n", + " high.map(|(d, b)| (format!(\"cgroup memory.high (\{})\", d), b)),\n", + " max.map(|(d, b)| (format!(\"cgroup memory.max (\{})\", d), b)),\n", + " ]\n", + " .into_iter()\n", + " .flatten()\n", + " .min_by_key(|(_, b)| *b);\n", + " if let Some((label, bytes)) = observed \{\n", + " let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n", + " let source = match env \{\n", + " Some(requested) => format!(\n", + " \"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\",\n", + " effective, requested, label, bytes\n", + " ),\n", + " None => label,\n", + " \};\n", + " return (Some(effective), source);\n", + " \}\n", + " if let Some(bytes) = host_budget_darwin_physical() \{\n", + " let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n", + " let label = \"sysctl hw.memsize\";\n", + " let source = match env \{\n", + " Some(requested) => format!(\n", + " \"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\",\n", + " effective, requested, label, bytes\n", + " ),\n", + " None => label.to_string(),\n", + " \};\n", + " return (Some(effective), source);\n", + " \}\n", + " if let Some(requested) = env \{\n", + " return (\n", + " Some(requested),\n", + " format!(\n", + " \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n", + " requested\n", + " ),\n", + " );\n", + " \}\n", + " (\n", + " None,\n", + " format!(\n", + " \"unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os=\{\}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\",\n", + " std::env::consts::OS\n", + " ),\n", + " )\n", + "\}\n\n", + "fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", + " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + " let rel = self_cg\n", + " .lines()\n", + " .find_map(|l| l.strip_prefix(\"0::\"))\n", + " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n", + " let root = std::path::Path::new(\"/sys/fs/cgroup\");\n", + " let mut dir = root.join(&rel);\n", + " let mut best: Option<(u64, std::path::PathBuf)> = None;\n", + " loop \{\n", + " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) \{\n", + " let s = s.trim();\n", + " if s != \"max\" \{\n", + " if let Ok(v) = s.parse::() \{\n", + " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n", + " if take \{\n", + " best = Some((v, dir.clone()));\n", + " \}\n", + " \}\n", + " \}\n", + " \}\n", + " if dir == root || !dir.pop() \{\n", + " break;\n", + " \}\n", + " \}\n", + " best.map(|(v, d)| (d.display().to_string(), v))\n", + "\}\n\n", + "fn host_budget_darwin_physical() -> Option \{\n", + " if std::env::consts::OS != \"macos\" \{\n", + " return None;\n", + " \}\n", + " let out = std::process::Command::new(\"sysctl\")\n", + " .args([\"-n\", \"hw.memsize\"])\n", + " .output()\n", + " .ok()?;\n", + " if !out.status.success() \{\n", + " return None;\n", + " \}\n", + " String::from_utf8(out.stdout)\n", + " .ok()?\n", + " .trim()\n", + " .parse::()\n", + " .ok()\n", + " .filter(|v| *v > 0)\n", + "\}\n" + ) } fn rt_accelerator_demo_kernel() -> String { diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 3f38f189c31..c24d5a68142 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1361,8 +1361,7 @@ pub fn read_host_budget_resolution() -> HostBudgetResolution { /// RUNG: *mitigatable*. The `.dag` authority states the correct rule and the Rust path does not /// enforce it; nothing detects the divergence today. pub fn read_host_budget_bytes() -> (Option, String) { - let resolution = read_host_budget_resolution(); - (resolution.bytes(), resolution.label()) + crate::v1_rt::read_host_budget_bytes() } /// leaf→root walk — the effective budget the OOM-killer enforces. `None` when unreadable diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 6f0da753052..45ccad5e8c0 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39235,7 +39235,7 @@ pub fn emit_shell_stderr_policy_binding( source_indices: Rc>>, ) -> String { if op_has_stderr_capture_param(op_node, source_indices) { - "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\") {\n Ok(raw) => Some(raw.parse::().map_err(|cause| format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: {}\", cause))?),\n Err(cause) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({})\", cause).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() + "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match v1_rt::read_host_budget_bytes() {\n (Some(n), _) => Some(n as usize),\n (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() } else { v1_rt::concat( v1_rt::concat( diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index fb2f5c5ae85..be448db0427 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -164,10 +164,114 @@ pub fn rust_runtime_source() -> String { ), rt_realization_measurement(), ), - rt_accelerator_demo_kernel(), + v1_rt::concat(rt_accelerator_demo_kernel(), rt_host_budget()), ) } +pub fn rt_host_budget() -> String { + "/// Host memory planning ceiling as `(bytes, source label)`.\n\ + /// Authority: `gunbc.host_budget_source`. Same join as the interpreter.\n\ + pub fn read_host_budget_bytes() -> (Option, String) {\n\ + let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n\ + .ok()\n\ + .and_then(|s| s.trim().parse::().ok());\n\ + let high = host_budget_tightest_cgroup(\"memory.high\");\n\ + let max = host_budget_tightest_cgroup(\"memory.max\");\n\ + let observed = [\n\ + high.map(|(d, b)| (format!(\"cgroup memory.high ({})\", d), b)),\n\ + max.map(|(d, b)| (format!(\"cgroup memory.max ({})\", d), b)),\n\ + ]\n\ + .into_iter()\n\ + .flatten()\n\ + .min_by_key(|(_, b)| *b);\n\ + if let Some((label, bytes)) = observed {\n\ + let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n\ + let source = match env {\n\ + Some(requested) => format!(\n\ + \"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\",\n\ + effective, requested, label, bytes\n\ + ),\n\ + None => label,\n\ + };\n\ + return (Some(effective), source);\n\ + }\n\ + if let Some(bytes) = host_budget_darwin_physical() {\n\ + let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n\ + let label = \"sysctl hw.memsize\";\n\ + let source = match env {\n\ + Some(requested) => format!(\n\ + \"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\",\n\ + effective, requested, label, bytes\n\ + ),\n\ + None => label.to_string(),\n\ + };\n\ + return (Some(effective), source);\n\ + }\n\ + if let Some(requested) = env {\n\ + return (\n\ + Some(requested),\n\ + format!(\n\ + \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n\ + requested\n\ + ),\n\ + );\n\ + }\n\ + (\n\ + None,\n\ + format!(\n\ + \"unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\",\n\ + std::env::consts::OS\n\ + ),\n\ + )\n\ + }\n\n\ + fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n\ + let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n\ + let rel = self_cg\n\ + .lines()\n\ + .find_map(|l| l.strip_prefix(\"0::\"))\n\ + .map(|p| p.trim().trim_start_matches('/').to_string())?;\n\ + let root = std::path::Path::new(\"/sys/fs/cgroup\");\n\ + let mut dir = root.join(&rel);\n\ + let mut best: Option<(u64, std::path::PathBuf)> = None;\n\ + loop {\n\ + if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n\ + let s = s.trim();\n\ + if s != \"max\" {\n\ + if let Ok(v) = s.parse::() {\n\ + let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n\ + if take {\n\ + best = Some((v, dir.clone()));\n\ + }\n\ + }\n\ + }\n\ + }\n\ + if dir == root || !dir.pop() {\n\ + break;\n\ + }\n\ + }\n\ + best.map(|(v, d)| (d.display().to_string(), v))\n\ + }\n\n\ + fn host_budget_darwin_physical() -> Option {\n\ + if std::env::consts::OS != \"macos\" {\n\ + return None;\n\ + }\n\ + let out = std::process::Command::new(\"sysctl\")\n\ + .args([\"-n\", \"hw.memsize\"])\n\ + .output()\n\ + .ok()?;\n\ + if !out.status.success() {\n\ + return None;\n\ + }\n\ + String::from_utf8(out.stdout)\n\ + .ok()?\n\ + .trim()\n\ + .parse::()\n\ + .ok()\n\ + .filter(|v| *v > 0)\n\ + }\n" + .to_string() +} + pub fn rt_accelerator_demo_kernel() -> String { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("fn int_relu(x: i64) -> i64 {\n".to_string(), " if x > 0 { x } else { 0 }\n".to_string()), "}\n\n".to_string()), "/// Opcode wire authority: `extdeps.languages.simd.kernel.relu_mul_add_wire_op_codes`.\n".to_string()), "/// Scaffold regen-sync: see `host_kernel_relu_mul_add_op_codes_scaffold_note` until feature:dag-kernel-realization-handler.\n".to_string()), "const RELU_MUL_ADD_OP_CODES: [i64; 3] = [1, 2, 3];\n\n".to_string()), "fn assert_relu_mul_add_op_codes(op_codes: &[i64], _handler: &str) {\n".to_string()), " if op_codes != RELU_MUL_ADD_OP_CODES {\n".to_string()), " panic!(\"unsupported op_codes in accelerator demo kernel\");\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Host bridge: `.dag` passes `std.numerical_contract.FmaContractionPolicy`; interpreter maps to 0/1 until target_model emit.\n".to_string()), "pub fn contiguous_loop_elementwise_float_kernel(\n".to_string()), " op_codes: &[i64],\n".to_string()), " fma_contraction_policy: i64,\n".to_string()), " a: &[f64],\n".to_string()), " b: &[f64],\n".to_string()), " c: &[f64],\n".to_string()), ") -> Vec {\n".to_string()), " assert_relu_mul_add_op_codes(op_codes, \"contiguous_loop_elementwise_float_kernel\");\n".to_string()), " assert_eq!(a.len(), b.len());\n".to_string()), " assert_eq!(b.len(), c.len());\n".to_string()), " let mut out = Vec::with_capacity(a.len());\n".to_string()), " for i in 0..a.len() {\n".to_string()), " let elem = match fma_contraction_policy {\n".to_string()), " 0 => {\n".to_string()), " let prod = a[i] * b[i];\n".to_string()), " prod + c[i]\n".to_string()), " },\n".to_string()), " 1 => a[i].mul_add(b[i], c[i]),\n".to_string()), " _ => panic!(\"unknown fma_contraction_policy\"),\n".to_string()), " };\n".to_string()), " out.push(if elem > 0.0 { elem } else { 0.0 });\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n\n".to_string()), "/// Integer oracle authority: `gunbc.accelerator_demo_eval` via interpreter `Int` binops (`*`/`+`).\n".to_string()), "/// Host kernel uses `wrapping_*` to mirror release interpreter overflow semantics; demo fixtures\n".to_string()), "/// stay in-range under `IntegerExact` — out-of-range inputs are outside the bit-exact bar.\n".to_string()), "pub fn contiguous_loop_elementwise_kernel(\n".to_string()), " op_codes: &[i64],\n".to_string()), " a: &[i64],\n".to_string()), " b: &[i64],\n".to_string()), " c: &[i64],\n".to_string()), ") -> Vec {\n".to_string()), " assert_relu_mul_add_op_codes(op_codes, \"contiguous_loop_elementwise_kernel\");\n".to_string()), " assert_eq!(a.len(), b.len());\n".to_string()), " assert_eq!(b.len(), c.len());\n".to_string()), " let mut out = Vec::with_capacity(a.len());\n".to_string()), " for i in 0..a.len() {\n".to_string()), " let tmp = a[i].wrapping_mul(b[i]).wrapping_add(c[i]);\n".to_string()), " out.push(int_relu(tmp));\n".to_string()), " }\n".to_string()), " out\n".to_string()), "}\n".to_string()) } diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index c3a9e9bf3e1..c4e2956d5fa 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1614,3 +1614,106 @@ pub fn contiguous_loop_elementwise_kernel( } out } + +/// Host memory planning ceiling as `(bytes, source label)`. +/// Authority: `gunbc.host_budget_source`. Same join as the interpreter. +pub fn read_host_budget_bytes() -> (Option, String) { + let env = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") + .ok() + .and_then(|s| s.trim().parse::().ok()); + let high = host_budget_tightest_cgroup("memory.high"); + let max = host_budget_tightest_cgroup("memory.max"); + let observed = [ + high.map(|(d, b)| (format!("cgroup memory.high ({})", d), b)), + max.map(|(d, b)| (format!("cgroup memory.max ({})", d), b)), + ] + .into_iter() + .flatten() + .min_by_key(|(_, b)| *b); + if let Some((label, bytes)) = observed { + let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes); + let source = match env { + Some(requested) => format!( + "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", + effective, requested, label, bytes + ), + None => label, + }; + return (Some(effective), source); + } + if let Some(bytes) = host_budget_darwin_physical() { + let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes); + let label = "sysctl hw.memsize"; + let source = match env { + Some(requested) => format!( + "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", + effective, requested, label, bytes + ), + None => label.to_string(), + }; + return (Some(effective), source); + } + if let Some(requested) = env { + return ( + Some(requested), + format!( + "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit", + requested + ), + ); + } + ( + None, + format!( + "unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", + std::env::consts::OS + ), + ) +} + +fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { + let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; + let rel = self_cg + .lines() + .find_map(|l| l.strip_prefix("0::")) + .map(|p| p.trim().trim_start_matches('/').to_string())?; + let root = std::path::Path::new("/sys/fs/cgroup"); + let mut dir = root.join(&rel); + let mut best: Option<(u64, std::path::PathBuf)> = None; + loop { + if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) { + let s = s.trim(); + if s != "max" { + if let Ok(v) = s.parse::() { + let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true); + if take { + best = Some((v, dir.clone())); + } + } + } + } + if dir == root || !dir.pop() { + break; + } + } + best.map(|(v, d)| (d.display().to_string(), v)) +} + +fn host_budget_darwin_physical() -> Option { + if std::env::consts::OS != "macos" { + return None; + } + let out = std::process::Command::new("sysctl") + .args(["-n", "hw.memsize"]) + .output() + .ok()?; + if !out.status.success() { + return None; + } + String::from_utf8(out.stdout) + .ok()? + .trim() + .parse::() + .ok() + .filter(|v| *v > 0) +} From a2935a4fcb9a59b5827e34ea37499144dbbfe437 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 14:59:08 +0000 Subject: [PATCH 07/27] Drop the unapproved seed-growth row and unused capture helpers. DESIGN section 5/6 refuse an author-declared scaffold approval; keep capture-accounting membership in one predicate. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 40 ------------------- dag/gunbc/seed_growth_admission.dag | 2 - src/v1/05_emit.dag | 9 ++--- src/v1/05_emit_rust.dag | 17 +------- src/v1/stage0/src/cli_run/emit_host.rs | 27 +++++++------ src/v1/stage0/src/v1_compiler_emit.rs | 9 ++--- src/v1/stage0/src/v1_compiler_emit_rust.rs | 28 ++----------- 7 files changed, 28 insertions(+), 104 deletions(-) delete mode 100644 dag/gunbc/rust_shell_stderr_capture_seed_growth.dag diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag deleted file mode 100644 index 1b0d164b02b..00000000000 --- a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag +++ /dev/null @@ -1,40 +0,0 @@ -module gunbc.rust_shell_stderr_capture_seed_growth - -import gunbc.roadmap_model { RoadmapNodeId } -import gunbc.seed_growth { SeedGrowthJustification } -import std.decl_ref { DeclarationRef, WholeDeclaration } - -// Seed-growth obligation for the rust shell stderr-capture realization (bright-tern-639 / #13472). -// Prepared beside the obligation it declares. SeedFeatureCompletion FIRES: the rust emitter -// gains a concurrent drain that binds WitnessStderrCapturePolicy and projects the capture -// channels. That class needs an exact-scope operator ruling, as -// gunbc.modeled_operation_realization_seed_growth modeled_operation_realization_seed_growth_justification -// did. This row does not mint the ruling; it states the items, the model authority, the -// boundary, the owner and the deletion trigger so a ruling has a subject. -// -// PURPOSE PASSES (gunbc.v1_maintenance_standing v1_seed_standing): the fixture-closure union -// must emit extdeps.gunbc WitnessBin.Run or the required floor stays red after #13437, which -// blocks the D2 self-host lane. The seed is the only rust shell emitter. -// -// THE FIVE REFUSED CLASSES. NewLanguageBehavior: no. NewCompatibilityObligation: no. -// NewEscapeHatchOrAdmissionRow: no — absent policy and Complete overflow refuse. -// SeedFeatureCompletion FIRES (ruling outstanding). PublicSurfaceGrowth: the emit_host -// additions are cfg(test) discriminators, not a published CLI. -// -// Hand items below are the emit_host executing harness. The emitter body lives in -// src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. -data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { - hand_authored_declarations: [ - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_under_cap_is_complete", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_over_budget_refuses", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_under_budget_retains_all", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } - ], - reason: "The rust shell handler must realize stderr_truncated / stderr_total_bytes / stderr_retained_bytes under the authored WitnessStderrCapturePolicy so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process; a .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", - owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, - trigger: "Delete these items when a .dag witness compiles the emitted rust crate and runs the same six discriminators without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", - current_boundary: "Bind and validate WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without the typed policy refuse at the rust emit diagnostic and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." -} diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index 2a8b059f6f7..1cece126858 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -81,7 +81,6 @@ import gunbc.floor_corpus_census_allowance_seed_growth { floor_corpus_census_all import gunbc.enrolment_dead_band_seed_growth { enrolment_dead_band_seed_growth_justification } import gunbc.host_speed_calibration_seed_growth { host_speed_calibration_seed_growth_justification } import gunbc.modeled_operation_realization_seed_growth { modeled_operation_realization_seed_growth_justification } -import gunbc.rust_shell_stderr_capture_seed_growth { rust_shell_stderr_capture_seed_growth_justification } import gunbc.regen_convergence_seed_growth { regen_convergence_seed_growth_justification } import gunbc.dag_artifact_identity_seed_growth { dag_artifact_identity_seed_growth_justification } import gunbc.regen_scope_worktree_seed_growth { regen_scope_worktree_seed_growth_justification } @@ -367,7 +366,6 @@ fn seed_growth_justification_roster() -> List { enrolment_dead_band_seed_growth_justification, host_speed_calibration_seed_growth_justification, modeled_operation_realization_seed_growth_justification, - rust_shell_stderr_capture_seed_growth_justification, observation_file_effect_seed_growth_justification, floor_checker_input_seed_growth_justification, match_arm_guard_seed_growth_justification, diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index 992a666b4bf..17e2b62e7d3 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1394,11 +1394,10 @@ fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> Bool { } fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> Bool { - match c { - ShellChanStderrTruncated => target_is_rust(target: target) - ShellChanStderrTotalBytes => target_is_rust(target: target) - ShellChanStderrRetainedBytes => target_is_rust(target: target) - _ => target_renders_shell_transport(target: target) + if shell_channel_is_capture_accounting(c: c) { + target_is_rust(target: target) + } else { + target_renders_shell_transport(target: target) } } diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index f4ecf741aa5..b65c7b8d6d1 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -268,6 +268,7 @@ import v1.compiler.emit { ShellChanExitSuccess, ShellChanExitCode, ShellChanStdoutLines, ShellChanStderrTruncated, ShellChanStderrTotalBytes, ShellChanStderrRetainedBytes, ShellChannelNotRealizedByTarget, ShellCapturePolicyInputAbsent, shell_emission_refusal_fact, shell_result_channel_key, + shell_channel_is_capture_accounting, bind_operation_transport, transport_binding_refusal_fact, FileVerb, FileRead, FileWrite, FileWriteOwnerOnly, FileWriteCreateNew, FileWriteCreateNewWithMode, FileLinkCreateNew, FileDelete, FileList, FileResultField, FileResultChannel, FileChanSuccess, FileChanByteCount, @@ -17056,12 +17057,7 @@ fn emit_shell_argv_element(arg: Node, optional_params: Map, source data shell_stderr_binding_line: String = "let stderr = String::from_utf8_lossy(&output.stderr).to_string();" fn shell_needs_capture_accounting(result_fields: List) -> Bool { - result_fields |> any(f => match f.channel { - ShellChanStderrTruncated => true - ShellChanStderrTotalBytes => true - ShellChanStderrRetainedBytes => true - _ => false - }) + result_fields |> any(f => shell_channel_is_capture_accounting(c: f.channel)) } fn op_has_stderr_capture_param(op_node: Node, source_indices: Map) -> Bool { @@ -17072,12 +17068,6 @@ fn shell_error_stderr_binding(result_fields: List) -> String { if shell_needs_capture_accounting(result_fields: result_fields) { "" } else { concat(shell_stderr_binding_line, " ") } } -// Concurrent drain of the child's stderr under the declared policy. Never -// wait_with_output-then-truncate: the tail is retained while the child runs. -fn emit_shell_capture_wait(op_node: Node, source_indices: Map) -> String { - concat(shell_capture_drain_start, shell_capture_join_project) -} - fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { if op_has_stderr_capture_param(op_node: op_node, source_indices: source_indices) { concat( @@ -17112,9 +17102,6 @@ data shell_capture_drain_start: String = "let mut stdout_pipe = output.stdout.ta data shell_capture_join_project: String = "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" -// Concurrent drain of stdout/stderr. Callers MUST emit a __stdin_thread before join. -data shell_capture_drain_body: String = concat(shell_capture_drain_start, "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n", shell_capture_join_project) - // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. // diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index bc33afa4cda..90a03c4c9ef 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3487,7 +3487,6 @@ mod fixture_closure_union_tests { stderr_payload: &str, stdin_payload: Option<&str>, ) -> std::process::Output { - let body = crate::v1_compiler_emit_rust::shell_capture_drain_body(); let limit = match complete_limit { Some(n) => format!("Some({n})"), None => "None".to_string(), @@ -3512,18 +3511,22 @@ mod fixture_closure_union_tests { ), ) } - None => ("printf %s '{payload}' 1>&2".to_string(), String::new()), - }; - let drain = if stdin_payload.is_some() { - format!( - "{}{}{}", - crate::v1_compiler_emit_rust::shell_capture_drain_start(), - stdin_prelude, - crate::v1_compiler_emit_rust::shell_capture_join_project() - ) - } else { - body + None => ( + "printf %s '{payload}' 1>&2".to_string(), + "let mut stdin_pipe = output.stdin.take();\n\ + let __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n\ + drop(stdin_pipe);\n\ + Ok(())\n\ + });\n" + .to_string(), + ), }; + let drain = format!( + "{}{}{}", + crate::v1_compiler_emit_rust::shell_capture_drain_start(), + stdin_prelude, + crate::v1_compiler_emit_rust::shell_capture_join_project() + ); let program = format!( "fn main() -> Result<(), Box> {{\n\ let __stderr_complete_limit: Option = {limit};\n\ diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 637b0eb0645..f3e34395019 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -2903,11 +2903,10 @@ pub fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> bool { } pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { - match c.clone() { - ShellResultChannel::ShellChanStderrTruncated => target_is_rust(target.clone()), - ShellResultChannel::ShellChanStderrTotalBytes => target_is_rust(target.clone()), - ShellResultChannel::ShellChanStderrRetainedBytes => target_is_rust(target.clone()), - _ => target_renders_shell_transport(target.clone()), + if shell_channel_is_capture_accounting(c.clone()) { + target_is_rust(target.clone()) + } else { + target_renders_shell_transport(target.clone()) } } diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 45ccad5e8c0..57c1d313b7b 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39195,14 +39195,9 @@ pub fn emit_shell_call( } pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { - result_fields.iter().any(|f| { - matches!( - f.channel, - ShellResultChannel::ShellChanStderrTruncated - | ShellResultChannel::ShellChanStderrTotalBytes - | ShellResultChannel::ShellChanStderrRetainedBytes - ) - }) + result_fields + .iter() + .any(|f| crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone())) } pub fn op_has_stderr_capture_param( @@ -39223,13 +39218,6 @@ pub fn shell_error_stderr_binding(result_fields: Rc>>) } } -pub fn emit_shell_capture_wait( - _op_node: Rc, - _source_indices: Rc>>, -) -> String { - v1_rt::concat(shell_capture_drain_start(), shell_capture_join_project()) -} - pub fn emit_shell_stderr_policy_binding( op_node: Rc, source_indices: Rc>>, @@ -39259,16 +39247,6 @@ pub fn shell_capture_join_project() -> String { "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() } -pub fn shell_capture_drain_body() -> String { - v1_rt::concat( - v1_rt::concat( - shell_capture_drain_start(), - "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string(), - ), - shell_capture_join_project(), - ) -} - pub fn shell_argv_param_is_word_list( param: Rc, source_indices: Rc>>, From adedd4693b773f742769f9f4ad723b86ada122ce Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 15:01:29 +0000 Subject: [PATCH 08/27] Record the 2026-10-06 seed-growth ruling for rust stderr capture. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 43 +++++++++++++++++++ dag/gunbc/seed_growth_admission.dag | 4 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 dag/gunbc/rust_shell_stderr_capture_seed_growth.dag diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag new file mode 100644 index 00000000000..26771ad6098 --- /dev/null +++ b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag @@ -0,0 +1,43 @@ +module gunbc.rust_shell_stderr_capture_seed_growth + +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.seed_growth { SeedGrowthJustification } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +// Seed-growth obligation for rust shell stderr capture (#13472), homed beside the +// obligation it declares rather than inside gunbc.seed_growth_admission, which owns +// the roster and the join. +// +// THE AUTHORIZER. Operator ruling 2026-10-06 (escalation msg_7853a1af, via +// silent-lark-156): "Admit, exact scope with seed-growth row". The admitted +// SeedFeatureCompletion is scoped to the rust shell handler realizing +// WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / +// stderr_retained_bytes) and the emit_host.rs tests that compile and run that +// fragment. This row records that ruling; it does not mint a second one. +// +// PURPOSE PASSES (gunbc.v1_maintenance_standing v1_seed_standing): the fixture-closure +// union must emit extdeps.gunbc WitnessBin.Run or the required floor stays red after +// #13437, which blocks the D2 self-host lane. The seed is the only rust shell emitter. +// +// THE FIVE REFUSED CLASSES. NewLanguageBehavior: no. NewCompatibilityObligation: no. +// NewEscapeHatchOrAdmissionRow: no — absent policy and Complete overflow refuse. +// SeedFeatureCompletion FIRES and is admitted on the ruling above. PublicSurfaceGrowth: +// the emit_host additions are cfg(test) discriminators, not a published CLI. +// +// Hand items below are the emit_host executing harness. The emitter body lives in +// src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. +data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_under_cap_is_complete", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_over_budget_refuses", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_under_budget_retains_all", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } + ], + reason: "Operator ruling 2026-10-06 (msg_7853a1af): admit SeedFeatureCompletion, exact scope the rust shell handler realizing WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / stderr_retained_bytes) and its emit_host.rs tests. The rust shell handler must realize those channels so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process; a .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", + owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, + trigger: "Delete these items when the v2-native emitter realizes the shell transport's stderr capture, a .dag witness compiles the emitted rust crate and runs the same six discriminators without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", + current_boundary: "Bind and validate WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without the typed policy refuse at the rust emit diagnostic and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." +} diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index 1cece126858..686a2551b8c 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -90,6 +90,7 @@ import gunbc.floor_checker_input_seed_growth { floor_checker_input_seed_growth_j import gunbc.data_eval_counters_seed_growth { data_eval_counters_seed_growth_justification } import gunbc.param_binder_seed_growth { param_binder_seed_growth_justification } import gunbc.process_lifecycle_seed_growth { process_lifecycle_seed_growth_justification } +import gunbc.rust_shell_stderr_capture_seed_growth { rust_shell_stderr_capture_seed_growth_justification } import gunbc.rust_item_host_observation { ItemDeclarationRefProjection, LiveHandRustObservation, @@ -371,7 +372,8 @@ fn seed_growth_justification_roster() -> List { match_arm_guard_seed_growth_justification, value_depth_seed_growth_justification, value_drop_binding_adaptation_seed_growth_justification, - process_lifecycle_seed_growth_justification + process_lifecycle_seed_growth_justification, + rust_shell_stderr_capture_seed_growth_justification ] } From b5e75b9eca54aa589445d0570e680363241118a9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 15:24:36 +0000 Subject: [PATCH 09/27] Restore the host-budget join, including cgroup v1, for interpreter and emit. Co-authored-by: Cursor --- src/v1/05_emit.dag | 3 +- src/v1/runtime_rust.dag | 89 +++++++++++++- src/v1/stage0/src/memory_governor.rs | 27 +++-- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 89 +++++++++++++- src/v1/stage0/src/v1_rt.rs | 109 +++++++++++++++++- 5 files changed, 296 insertions(+), 21 deletions(-) diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index 17e2b62e7d3..6d88497e091 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1380,7 +1380,8 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // realization handler (v1.compiler.emit_rust emit_shell_call) implements the // declared WitnessStderrCapturePolicy: concurrent drain, BoundedTail retains a // tail, Complete retains within the host budget -// (`v1_rt::read_host_budget_bytes`, the same join as the interpreter) or +// (`v1_rt::read_host_budget_bytes`, the `(bytes, label)` view over the same +// live observations `memory_governor::read_host_budget_resolution` consumes) or // refuses. Python and go still realize no capture policy, so they refuse these // three at field grain rather than answering false / raw length (that answer // would assert the declared policy ran). diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 1f93e5465e8..216e5d48a9c 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -1310,16 +1310,36 @@ fn rust_runtime_source() -> String { fn rt_host_budget() -> String { concat( "/// Host memory planning ceiling as `(bytes, source label)`.\n", - "/// Authority: `gunbc.host_budget_source`. Same join as the interpreter.\n", + "/// Authority: `gunbc.host_budget_source`. Observations are the live reads\n", + "/// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of\n", + "/// the same precedence, including cgroup v1 `hierarchical_memory_limit`.\n", "pub fn read_host_budget_bytes() -> (Option, String) \{\n", " let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n", " .ok()\n", " .and_then(|s| s.trim().parse::().ok());\n", + " let v1_reading = host_budget_cgroup_v1();\n", + " if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() \{\n", + " return (\n", + " None,\n", + " format!(\n", + " \"unreadable: cgroup v1 memory hierarchy at \{\} holds this process but its hierarchical_memory_limit is unreadable (\{\}); a bound that may be the tightest cannot be replaced by another reading\",\n", + " dir, body\n", + " ),\n", + " );\n", + " \}\n", " let high = host_budget_tightest_cgroup(\"memory.high\");\n", " let max = host_budget_tightest_cgroup(\"memory.max\");\n", + " let v1 = match v1_reading \{\n", + " Some((d, HostBudgetCgroupV1::Limited(b))) => Some((\n", + " format!(\"cgroup v1 memory.stat hierarchical_memory_limit (\{\})\", d),\n", + " b,\n", + " )),\n", + " _ => None,\n", + " \};\n", " let observed = [\n", " high.map(|(d, b)| (format!(\"cgroup memory.high (\{})\", d), b)),\n", " max.map(|(d, b)| (format!(\"cgroup memory.max (\{})\", d), b)),\n", + " v1,\n", " ]\n", " .into_iter()\n", " .flatten()\n", @@ -1351,7 +1371,7 @@ fn rt_host_budget() -> String { " return (\n", " Some(requested),\n", " format!(\n", - " \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n", + " \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n", " requested\n", " ),\n", " );\n", @@ -1364,7 +1384,13 @@ fn rt_host_budget() -> String { " ),\n", " )\n", "\}\n\n", - "fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", + "#[derive(Clone)]\n", + "pub enum HostBudgetCgroupV1 \{\n", + " Limited(u64),\n", + " Unlimited,\n", + " Unparseable(String),\n", + "\}\n\n", + "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", " let rel = self_cg\n", " .lines()\n", @@ -1391,7 +1417,62 @@ fn rt_host_budget() -> String { " \}\n", " best.map(|(v, d)| (d.display().to_string(), v))\n", "\}\n\n", - "fn host_budget_darwin_physical() -> Option \{\n", + "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> \{\n", + " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n", + " let page_size = unsafe \{ libc::sysconf(libc::_SC_PAGESIZE) \};\n", + " if page_size <= 0 \{\n", + " return Some((\n", + " \"/proc/self/mountinfo\".to_string(),\n", + " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n", + " ));\n", + " \}\n", + " let page_size = page_size as u64;\n", + " let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n", + " let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) \{\n", + " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n", + " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: \{\}\", e)),\n", + " \};\n", + " Some((dir, value))\n", + "\}\n\n", + "fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option \{\n", + " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| \{\n", + " let fields: Vec<&str> = line.split(' ').collect();\n", + " let dash = fields.iter().position(|f| *f == \"-\")?;\n", + " let fstype = fields.get(dash + 1)?;\n", + " let super_opts = fields.get(dash + 3)?;\n", + " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") \{\n", + " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n", + " \} else \{\n", + " None\n", + " \}\n", + " \})?;\n", + " let path = self_cg.lines().find_map(|l| \{\n", + " let mut parts = l.splitn(3, ':');\n", + " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n", + " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n", + " \})?;\n", + " let rel = if mount_root == \"/\" \{\n", + " path.as_str()\n", + " \} else \{\n", + " let rest = path.strip_prefix(mount_root.as_str())?;\n", + " if !(rest.is_empty() || rest.starts_with('/')) \{ return None; \}\n", + " rest\n", + " \};\n", + " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n", + "\}\n\n", + "fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 \{\n", + " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n", + " let [body] = hits.as_slice() else \{ return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); \};\n", + " let unlimited = (i64::MAX as u64 / page_size) * page_size;\n", + " match body.trim().parse::() \{\n", + " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", + " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n", + " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n", + " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", + " \}\n", + "\}\n\n", + "pub fn host_budget_darwin_physical() -> Option \{\n", " if std::env::consts::OS != \"macos\" \{\n", " return None;\n", " \}\n", diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index c24d5a68142..e416732cdc7 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1317,18 +1317,28 @@ pub fn read_host_budget_resolution() -> HostBudgetResolution { let env_override = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") .ok() .and_then(|s| s.trim().parse::().ok()); - let cgroup_high = binding_high_cgroup_dir().and_then(|dir| { - read_cgroup_u64(&dir, "memory.high").map(|v| (dir.display().to_string(), v)) - }); - let cgroup_max = binding_cap_cgroup_dir().and_then(|dir| { - read_cgroup_u64(&dir, "memory.max").map(|v| (dir.display().to_string(), v)) + let cgroup_high = crate::v1_rt::host_budget_tightest_cgroup("memory.high"); + let cgroup_max = crate::v1_rt::host_budget_tightest_cgroup("memory.max"); + let cgroup_v1_limit = crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) }); resolve_host_budget( env_override, cgroup_high, cgroup_max, - read_cgroup_v1_hierarchical_limit(), - darwin_physical_memory_bytes(), + cgroup_v1_limit, + crate::v1_rt::host_budget_darwin_physical(), ) } @@ -1361,7 +1371,8 @@ pub fn read_host_budget_resolution() -> HostBudgetResolution { /// RUNG: *mitigatable*. The `.dag` authority states the correct rule and the Rust path does not /// enforce it; nothing detects the divergence today. pub fn read_host_budget_bytes() -> (Option, String) { - crate::v1_rt::read_host_budget_bytes() + let resolution = read_host_budget_resolution(); + (resolution.bytes(), resolution.label()) } /// leaf→root walk — the effective budget the OOM-killer enforces. `None` when unreadable diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index be448db0427..90fea84b8c2 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -170,16 +170,36 @@ pub fn rust_runtime_source() -> String { pub fn rt_host_budget() -> String { "/// Host memory planning ceiling as `(bytes, source label)`.\n\ - /// Authority: `gunbc.host_budget_source`. Same join as the interpreter.\n\ + /// Authority: `gunbc.host_budget_source`. Observations are the live reads\n\ + /// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of\n\ + /// the same precedence, including cgroup v1 `hierarchical_memory_limit`.\n\ pub fn read_host_budget_bytes() -> (Option, String) {\n\ let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n\ .ok()\n\ .and_then(|s| s.trim().parse::().ok());\n\ + let v1_reading = host_budget_cgroup_v1();\n\ + if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() {\n\ + return (\n\ + None,\n\ + format!(\n\ + \"unreadable: cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\",\n\ + dir, body\n\ + ),\n\ + );\n\ + }\n\ let high = host_budget_tightest_cgroup(\"memory.high\");\n\ let max = host_budget_tightest_cgroup(\"memory.max\");\n\ + let v1 = match v1_reading {\n\ + Some((d, HostBudgetCgroupV1::Limited(b))) => Some((\n\ + format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", d),\n\ + b,\n\ + )),\n\ + _ => None,\n\ + };\n\ let observed = [\n\ high.map(|(d, b)| (format!(\"cgroup memory.high ({})\", d), b)),\n\ max.map(|(d, b)| (format!(\"cgroup memory.max ({})\", d), b)),\n\ + v1,\n\ ]\n\ .into_iter()\n\ .flatten()\n\ @@ -211,7 +231,7 @@ pub fn rt_host_budget() -> String { return (\n\ Some(requested),\n\ format!(\n\ - \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n\ + \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n\ requested\n\ ),\n\ );\n\ @@ -224,7 +244,13 @@ pub fn rt_host_budget() -> String { ),\n\ )\n\ }\n\n\ - fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n\ + #[derive(Clone)]\n\ + pub enum HostBudgetCgroupV1 {\n\ + Limited(u64),\n\ + Unlimited,\n\ + Unparseable(String),\n\ + }\n\n\ + pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n\ let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n\ let rel = self_cg\n\ .lines()\n\ @@ -251,7 +277,62 @@ pub fn rt_host_budget() -> String { }\n\ best.map(|(v, d)| (d.display().to_string(), v))\n\ }\n\n\ - fn host_budget_darwin_physical() -> Option {\n\ + pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n\ + let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n\ + let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n\ + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n\ + if page_size <= 0 {\n\ + return Some((\n\ + \"/proc/self/mountinfo\".to_string(),\n\ + HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n\ + ));\n\ + }\n\ + let page_size = page_size as u64;\n\ + let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n\ + let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) {\n\ + Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n\ + Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n\ + };\n\ + Some((dir, value))\n\ + }\n\n\ + fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n\ + let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n\ + let fields: Vec<&str> = line.split(' ').collect();\n\ + let dash = fields.iter().position(|f| *f == \"-\")?;\n\ + let fstype = fields.get(dash + 1)?;\n\ + let super_opts = fields.get(dash + 3)?;\n\ + if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n\ + Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n\ + } else {\n\ + None\n\ + }\n\ + })?;\n\ + let path = self_cg.lines().find_map(|l| {\n\ + let mut parts = l.splitn(3, ':');\n\ + let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n\ + controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n\ + })?;\n\ + let rel = if mount_root == \"/\" {\n\ + path.as_str()\n\ + } else {\n\ + let rest = path.strip_prefix(mount_root.as_str())?;\n\ + if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n\ + rest\n\ + };\n\ + Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n\ + }\n\n\ + fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n\ + let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n\ + let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n\ + let unlimited = (i64::MAX as u64 / page_size) * page_size;\n\ + match body.trim().parse::() {\n\ + Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n\ + Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n\ + Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n\ + Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n\ + }\n\ + }\n\n\ + pub fn host_budget_darwin_physical() -> Option {\n\ if std::env::consts::OS != \"macos\" {\n\ return None;\n\ }\n\ diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index c4e2956d5fa..c1bd689ffd4 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1616,16 +1616,37 @@ pub fn contiguous_loop_elementwise_kernel( } /// Host memory planning ceiling as `(bytes, source label)`. -/// Authority: `gunbc.host_budget_source`. Same join as the interpreter. +/// Authority: `gunbc.host_budget_source`. Observations are the live reads +/// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of +/// the same precedence, including cgroup v1 `hierarchical_memory_limit`. pub fn read_host_budget_bytes() -> (Option, String) { let env = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") .ok() .and_then(|s| s.trim().parse::().ok()); + let v1_reading = host_budget_cgroup_v1(); + if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() { + return ( + None, + format!( + "unreadable: cgroup v1 memory hierarchy at {dir} holds this process but its \ + hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ + tightest cannot be replaced by another reading" + ), + ); + } let high = host_budget_tightest_cgroup("memory.high"); let max = host_budget_tightest_cgroup("memory.max"); + let v1 = match v1_reading { + Some((d, HostBudgetCgroupV1::Limited(b))) => Some(( + format!("cgroup v1 memory.stat hierarchical_memory_limit ({d})"), + b, + )), + _ => None, + }; let observed = [ high.map(|(d, b)| (format!("cgroup memory.high ({})", d), b)), max.map(|(d, b)| (format!("cgroup memory.max ({})", d), b)), + v1, ] .into_iter() .flatten() @@ -1657,7 +1678,7 @@ pub fn read_host_budget_bytes() -> (Option, String) { return ( Some(requested), format!( - "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit", + "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit", requested ), ); @@ -1671,7 +1692,14 @@ pub fn read_host_budget_bytes() -> (Option, String) { ) } -fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { +#[derive(Clone)] +pub enum HostBudgetCgroupV1 { + Limited(u64), + Unlimited, + Unparseable(String), +} + +pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; let rel = self_cg .lines() @@ -1699,7 +1727,80 @@ fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { best.map(|(v, d)| (d.display().to_string(), v)) } -fn host_budget_darwin_physical() -> Option { +pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> { + let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; + let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + if page_size <= 0 { + return Some(( + "/proc/self/mountinfo".to_string(), + HostBudgetCgroupV1::Unparseable("sysconf(_SC_PAGESIZE) unreadable".to_string()), + )); + } + let page_size = page_size as u64; + let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?; + let value = match std::fs::read_to_string(std::path::Path::new(&dir).join("memory.stat")) { + Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size), + Err(e) => HostBudgetCgroupV1::Unparseable(format!("memory.stat: {e}")), + }; + Some((dir, value)) +} + +fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { + let (mount_root, mount_point) = mountinfo.lines().find_map(|line| { + let fields: Vec<&str> = line.split(' ').collect(); + let dash = fields.iter().position(|f| *f == "-")?; + let fstype = fields.get(dash + 1)?; + let super_opts = fields.get(dash + 3)?; + if *fstype == "cgroup" && super_opts.split(',').any(|o| o == "memory") { + Some((fields.get(3)?.to_string(), fields.get(4)?.to_string())) + } else { + None + } + })?; + let path = self_cg.lines().find_map(|l| { + let mut parts = l.splitn(3, ':'); + let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?); + controllers + .split(',') + .any(|c| c == "memory") + .then(|| path.trim().to_string()) + })?; + let rel = if mount_root == "/" { + path.as_str() + } else { + let rest = path.strip_prefix(mount_root.as_str())?; + if !(rest.is_empty() || rest.starts_with('/')) { + return None; + } + rest + }; + Some( + std::path::Path::new(&mount_point) + .join(rel.trim_start_matches('/')) + .display() + .to_string(), + ) +} + +fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 { + let hits: std::vec::Vec<&str> = memory_stat + .lines() + .filter_map(|l| l.trim().strip_prefix("hierarchical_memory_limit ")) + .collect(); + let [body] = hits.as_slice() else { + return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); + }; + let unlimited = (i64::MAX as u64 / page_size) * page_size; + match body.trim().parse::() { + Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()), + Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited, + Ok(n) => HostBudgetCgroupV1::Limited(n as u64), + Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()), + } +} + +pub fn host_budget_darwin_physical() -> Option { if std::env::consts::OS != "macos" { return None; } From 82bc8b9304d84ac0d29c7f6eafbc8735ba424b62 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 15:26:38 +0000 Subject: [PATCH 10/27] Move emit_shell_call capture notes above the declaration. Inner // annotations refuse at parse (floor on adedd46). Co-authored-by: Cursor --- src/v1/05_emit_rust.dag | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b65c7b8d6d1..396768eae07 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -16875,6 +16875,9 @@ fn emit_exit_arm(prop: Node, result_fields: List, source_indic // replace one loud stop with another whose red this route cannot adjudicate (an emitted panic // COMPILES, so the fixture pairs beside this change would report it green). Only the LATER elements // admit both shapes; see shell_argv_element_is_word_list. +// Capture order: bind/validate the authored policy BEFORE spawn; start both drains +// immediately; write stdin concurrently; join; then project BoundedTail or refuse +// Complete overflow. fn emit_shell_call(op_name: String, transport: Node, registry: Map, depth: Int, result_fields: List, op_node: Node, source_indices: Map) -> String { let argv = transport.children let optional_params = op_node.params @@ -16916,8 +16919,6 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map Date: Tue, 6 Oct 2026 15:50:26 +0000 Subject: [PATCH 11/27] Compose the host budget only in v1_rt::resolve_host_budget_join. Co-authored-by: Cursor --- src/v1/05_emit.dag | 4 +- src/v1/runtime_rust.dag | 141 +++++------ src/v1/stage0/src/memory_governor.rs | 138 +++++------ src/v1/stage0/src/v1_compiler_runtime_rust.rs | 139 +++++------ src/v1/stage0/src/v1_rt.rs | 225 +++++++++++++----- 5 files changed, 364 insertions(+), 283 deletions(-) diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index 6d88497e091..d60b9346f31 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -1380,8 +1380,8 @@ fn shell_result_channel_key(c: ShellResultChannel) -> String { // realization handler (v1.compiler.emit_rust emit_shell_call) implements the // declared WitnessStderrCapturePolicy: concurrent drain, BoundedTail retains a // tail, Complete retains within the host budget -// (`v1_rt::read_host_budget_bytes`, the `(bytes, label)` view over the same -// live observations `memory_governor::read_host_budget_resolution` consumes) or +// (`v1_rt::read_host_budget_bytes` over `v1_rt::resolve_host_budget_join`, +// the one composer `memory_governor::resolve_host_budget` also calls) or // refuses. Python and go still realize no capture policy, so they refuse these // three at field grain rather than answering false / raw length (that answer // would assert the declared policy ran). diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 216e5d48a9c..9d7ab65edd6 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -1309,80 +1309,83 @@ fn rust_runtime_source() -> String { fn rt_host_budget() -> String { concat( - "/// Host memory planning ceiling as `(bytes, source label)`.\n", - "/// Authority: `gunbc.host_budget_source`. Observations are the live reads\n", - "/// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of\n", - "/// the same precedence, including cgroup v1 `hierarchical_memory_limit`.\n", - "pub fn read_host_budget_bytes() -> (Option, String) \{\n", - " let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n", - " .ok()\n", - " .and_then(|s| s.trim().parse::().ok());\n", - " let v1_reading = host_budget_cgroup_v1();\n", - " if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() \{\n", - " return (\n", - " None,\n", - " format!(\n", - " \"unreadable: cgroup v1 memory hierarchy at \{\} holds this process but its hierarchical_memory_limit is unreadable (\{\}); a bound that may be the tightest cannot be replaced by another reading\",\n", - " dir, body\n", - " ),\n", - " );\n", + "/// The one host-budget precedence. `read_host_budget_bytes` and\n", + "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n", + "#[derive(Clone, Debug, PartialEq, Eq)]\n", + "pub enum HostBudgetJoinSource \{\n", + " CgroupMemoryHigh \{ cgroup_dir: String \},\n", + " CgroupMemoryMax \{ cgroup_dir: String \},\n", + " CgroupV1HierarchicalMemoryLimit \{ cgroup_dir: String \},\n", + " DarwinPhysicalMemory,\n", + "\}\n\n", + "impl HostBudgetJoinSource \{\n", + " pub fn label(&self) -> String \{\n", + " match self \{\n", + " HostBudgetJoinSource::CgroupMemoryHigh \{ cgroup_dir \} => format!(\"cgroup memory.high (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::CgroupMemoryMax \{ cgroup_dir \} => format!(\"cgroup memory.max (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit \{ cgroup_dir \} => format!(\"cgroup v1 memory.stat hierarchical_memory_limit (\{\})\", cgroup_dir),\n", + " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n", + " \}\n", " \}\n", - " let high = host_budget_tightest_cgroup(\"memory.high\");\n", - " let max = host_budget_tightest_cgroup(\"memory.max\");\n", - " let v1 = match v1_reading \{\n", - " Some((d, HostBudgetCgroupV1::Limited(b))) => Some((\n", - " format!(\"cgroup v1 memory.stat hierarchical_memory_limit (\{\})\", d),\n", - " b,\n", - " )),\n", - " _ => None,\n", + "\}\n\n", + "#[derive(Clone, Debug, PartialEq, Eq)]\n", + "pub enum HostBudgetJoin \{\n", + " Resolved \{ effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 \},\n", + " DeclaredUnverified \{ requested_bytes: u64, reason: String \},\n", + " Unreadable \{ reason: String \},\n", + "\}\n\n", + "impl HostBudgetJoin \{\n", + " pub fn bytes(&self) -> Option \{\n", + " match self \{\n", + " HostBudgetJoin::Resolved \{ effective_bytes, .. \} => Some(*effective_bytes),\n", + " HostBudgetJoin::DeclaredUnverified \{ requested_bytes, .. \} => Some(*requested_bytes),\n", + " HostBudgetJoin::Unreadable \{ .. \} => None,\n", + " \}\n", + " \}\n", + " pub fn label(&self) -> String \{\n", + " match self \{\n", + " HostBudgetJoin::Resolved \{ effective_bytes, requested_bytes, source, observed_bytes \} => match requested_bytes \{\n", + " Some(requested) => format!(\"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n", + " None => source.label(),\n", + " \},\n", + " HostBudgetJoin::Unreadable \{ reason \} => format!(\"unreadable: \{\}\", reason),\n", + " HostBudgetJoin::DeclaredUnverified \{ requested_bytes, reason \} => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; \{\}\", requested_bytes, reason),\n", + " \}\n", + " \}\n", + "\}\n\n", + "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin \{\n", + " let cgroup_v1_limit = match cgroup_v1_limit \{\n", + " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => \{\n", + " return HostBudgetJoin::Unreadable \{ reason: format!(\"cgroup v1 memory hierarchy at \{\} holds this process but its hierarchical_memory_limit is unreadable (\{\}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) \};\n", + " \}\n", + " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n", + " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n", " \};\n", - " let observed = [\n", - " high.map(|(d, b)| (format!(\"cgroup memory.high (\{})\", d), b)),\n", - " max.map(|(d, b)| (format!(\"cgroup memory.max (\{})\", d), b)),\n", - " v1,\n", - " ]\n", - " .into_iter()\n", - " .flatten()\n", - " .min_by_key(|(_, b)| *b);\n", - " if let Some((label, bytes)) = observed \{\n", - " let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n", - " let source = match env \{\n", - " Some(requested) => format!(\n", - " \"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\",\n", - " effective, requested, label, bytes\n", - " ),\n", - " None => label,\n", - " \};\n", - " return (Some(effective), source);\n", + " let observation = [\n", + " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh \{ cgroup_dir \}, b)),\n", + " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax \{ cgroup_dir \}, b)),\n", + " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit \{ cgroup_dir \}, b)),\n", + " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best \{ Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) \});\n", + " if let Some((source, observed_bytes)) = observation \{\n", + " return HostBudgetJoin::Resolved \{ effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes \};\n", " \}\n", - " if let Some(bytes) = host_budget_darwin_physical() \{\n", - " let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n", - " let label = \"sysctl hw.memsize\";\n", - " let source = match env \{\n", - " Some(requested) => format!(\n", - " \"effective planning minimum \{\} bytes (env request \{\}; observed \{\}=\{\} bytes)\",\n", - " effective, requested, label, bytes\n", - " ),\n", - " None => label.to_string(),\n", - " \};\n", - " return (Some(effective), source);\n", + " if let Some(bytes) = darwin_physical \{\n", + " return HostBudgetJoin::Resolved \{ effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes \};\n", " \}\n", - " if let Some(requested) = env \{\n", - " return (\n", - " Some(requested),\n", - " format!(\n", - " \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES=\{\}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n", - " requested\n", - " ),\n", - " );\n", + " if let Some(requested_bytes) = env_override \{\n", + " return HostBudgetJoin::DeclaredUnverified \{ requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() \};\n", " \}\n", - " (\n", - " None,\n", - " format!(\n", - " \"unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os=\{\}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\",\n", - " std::env::consts::OS\n", - " ),\n", - " )\n", + " HostBudgetJoin::Unreadable \{ reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os=\{\}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) \}\n", + "\}\n\n", + "pub fn read_host_budget_bytes() -> (Option, String) \{\n", + " let join = resolve_host_budget_join(\n", + " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n", + " host_budget_tightest_cgroup(\"memory.high\"),\n", + " host_budget_tightest_cgroup(\"memory.max\"),\n", + " host_budget_cgroup_v1(),\n", + " host_budget_darwin_physical(),\n", + " );\n", + " (join.bytes(), join.label())\n", "\}\n\n", "#[derive(Clone)]\n", "pub enum HostBudgetCgroupV1 \{\n", diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index e416732cdc7..a9e4346f53f 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1218,17 +1218,10 @@ pub fn host_budget_unreadable_reason() -> String { ) } -/// Resolve the host budget from OBSERVATIONS, so every arm — including the refusal — is -/// reachable from a test on any machine. `read_host_budget_resolution` is this function -/// applied to the real reads; nothing else composes the precedence. -/// -/// The effective planning ceiling is the minimum of the operator request and every observed -/// applicable cgroup line. An operator request alone is `DeclaredUnverified`: an integer in an -/// environment variable constrains no allocation and is not evidence of executor provisioning. -/// There is no meminfo arm: MemAvailable and MemTotal describe a MACHINE, and -/// on a kernel that can express a private limit, substituting one for the limit this process -/// failed to read is DESIGN §5's absorbing fallback (answering with a superset). Authority: -/// `gunbc.host_budget_source` `host_budget_source_admissible_as_bound_on_kernel`. +/// Typed view of `v1_rt::resolve_host_budget_join`. That function is the one composer +/// (`gunbc.host_budget_source`); this maps its result onto `HostBudgetResolution` so +/// existing governor consumers keep a typed source. Tests plant observations here +/// and still exercise the runtime join. pub fn resolve_host_budget( env_override: Option, cgroup_high: Option<(String, u64)>, @@ -1236,83 +1229,68 @@ pub fn resolve_host_budget( cgroup_v1_limit: Option<(String, CgroupV1MemoryLimitValue)>, darwin_physical: Option, ) -> HostBudgetResolution { - // A v1 memory hierarchy holds this process but its limit could not be read: that limit may - // be the tightest one, so no other observation may stand in for it (DESIGN §5). - let cgroup_v1_limit = match cgroup_v1_limit { - Some((dir, CgroupV1MemoryLimitValue::Unparseable(body))) => { - return HostBudgetResolution::Unreadable { - reason: format!( - "cgroup v1 memory hierarchy at {dir} holds this process but its \ - hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ - tightest cannot be replaced by another reading" - ), - }; - } - Some((dir, CgroupV1MemoryLimitValue::Limited(bytes))) => Some((dir, bytes)), - Some((_, CgroupV1MemoryLimitValue::Unlimited)) | None => None, - }; - // Every observed process-scoped line is a candidate; the tightest one is the planning - // ceiling. On a hybrid host the unified hierarchy carries no memory files, so the v2 and - // v1 readings do not normally coexist; when they do, the minimum is still the honest bound. - let observation = [ - cgroup_high.map(|(cgroup_dir, b)| (HostBudgetSource::CgroupMemoryHigh { cgroup_dir }, b)), - cgroup_max.map(|(cgroup_dir, b)| (HostBudgetSource::CgroupMemoryMax { cgroup_dir }, b)), - cgroup_v1_limit.map(|(cgroup_dir, b)| { - ( - HostBudgetSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, - b, - ) - }), - ] - .into_iter() - .flatten() - .fold(None::<(HostBudgetSource, u64)>, |best, cand| match best { - Some(cur) if cur.1 <= cand.1 => Some(cur), - _ => Some(cand), + let v1 = cgroup_v1_limit.map(|(dir, v)| { + ( + dir, + match v { + CgroupV1MemoryLimitValue::Limited(bytes) => { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) + } + CgroupV1MemoryLimitValue::Unlimited => crate::v1_rt::HostBudgetCgroupV1::Unlimited, + CgroupV1MemoryLimitValue::Unparseable(body) => { + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) + } + }, + ) }); - if let Some((source, observed_bytes)) = observation { - return HostBudgetResolution::Resolved { - effective_bytes: env_override - .map(|requested| requested.min(observed_bytes)) - .unwrap_or(observed_bytes), - requested_bytes: env_override, + match crate::v1_rt::resolve_host_budget_join( + env_override, + cgroup_high, + cgroup_max, + v1, + darwin_physical, + ) { + crate::v1_rt::HostBudgetJoin::Resolved { + effective_bytes, + requested_bytes, + source, + observed_bytes, + } => HostBudgetResolution::Resolved { + effective_bytes, + requested_bytes, observation: HostBudgetObservation { - source, + source: match source { + crate::v1_rt::HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => { + HostBudgetSource::CgroupMemoryHigh { cgroup_dir } + } + crate::v1_rt::HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => { + HostBudgetSource::CgroupMemoryMax { cgroup_dir } + } + crate::v1_rt::HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { + cgroup_dir, + } => HostBudgetSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, + crate::v1_rt::HostBudgetJoinSource::DarwinPhysicalMemory => { + HostBudgetSource::DarwinPhysicalMemory + } + }, bytes: observed_bytes, }, - }; - } - // Darwin only. `darwin_physical_memory_bytes` is `None` on every other target, so this - // arm cannot be reached on a kernel that has cgroups — which is precisely the wall - // `host_budget_source_admissible_as_bound_on_kernel` states: a host-shared reading may - // serve as the budget only where no private-limit mechanism exists. - if let Some(bytes) = darwin_physical { - return HostBudgetResolution::Resolved { - effective_bytes: env_override - .map(|requested| requested.min(bytes)) - .unwrap_or(bytes), - requested_bytes: env_override, - observation: HostBudgetObservation { - source: HostBudgetSource::DarwinPhysicalMemory, - bytes, - }, - }; - } - if let Some(requested_bytes) = env_override { - return HostBudgetResolution::DeclaredUnverified { + }, + crate::v1_rt::HostBudgetJoin::DeclaredUnverified { requested_bytes, - reason: "no observed private memory.high or memory.max verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string(), - }; - } - HostBudgetResolution::Unreadable { - reason: host_budget_unreadable_reason(), + reason, + } => HostBudgetResolution::DeclaredUnverified { + requested_bytes, + reason, + }, + crate::v1_rt::HostBudgetJoin::Unreadable { reason } => { + HostBudgetResolution::Unreadable { reason } + } } } -/// The host memory planning ceiling, as a typed resolution. It does not cap RSS. Single authority shared -/// by the MemoryGovernor (which SCHEDULES against it), the typed-module cache cap (which -/// bounds an estimated ENTRY COUNT with it) and the P4 realize advisory (which PREDICTS against it) — no -/// consumer may re-read a partial version of this precedence (§3 single authority). +/// The host memory planning ceiling, as a typed resolution. Live observations and +/// precedence are `v1_rt::resolve_host_budget_join`; this is the typed wrapper. pub fn read_host_budget_resolution() -> HostBudgetResolution { let env_override = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") .ok() diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 90fea84b8c2..830a05dd15d 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -169,80 +169,81 @@ pub fn rust_runtime_source() -> String { } pub fn rt_host_budget() -> String { - "/// Host memory planning ceiling as `(bytes, source label)`.\n\ - /// Authority: `gunbc.host_budget_source`. Observations are the live reads\n\ - /// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of\n\ - /// the same precedence, including cgroup v1 `hierarchical_memory_limit`.\n\ - pub fn read_host_budget_bytes() -> (Option, String) {\n\ - let env = std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\")\n\ - .ok()\n\ - .and_then(|s| s.trim().parse::().ok());\n\ - let v1_reading = host_budget_cgroup_v1();\n\ - if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() {\n\ - return (\n\ - None,\n\ - format!(\n\ - \"unreadable: cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\",\n\ - dir, body\n\ - ),\n\ - );\n\ + "#[derive(Clone, Debug, PartialEq, Eq)]\n\ + pub enum HostBudgetJoinSource {\n\ + CgroupMemoryHigh { cgroup_dir: String },\n\ + CgroupMemoryMax { cgroup_dir: String },\n\ + CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n\ + DarwinPhysicalMemory,\n\ + }\n\n\ + impl HostBudgetJoinSource {\n\ + pub fn label(&self) -> String {\n\ + match self {\n\ + HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n\ + HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n\ + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n\ + HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n\ + }\n\ + }\n\ + }\n\n\ + #[derive(Clone, Debug, PartialEq, Eq)]\n\ + pub enum HostBudgetJoin {\n\ + Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n\ + DeclaredUnverified { requested_bytes: u64, reason: String },\n\ + Unreadable { reason: String },\n\ + }\n\n\ + impl HostBudgetJoin {\n\ + pub fn bytes(&self) -> Option {\n\ + match self {\n\ + HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n\ + HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n\ + HostBudgetJoin::Unreadable { .. } => None,\n\ + }\n\ + }\n\ + pub fn label(&self) -> String {\n\ + match self {\n\ + HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n\ + Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n\ + None => source.label(),\n\ + },\n\ + HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n\ + HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n\ + }\n\ }\n\ - let high = host_budget_tightest_cgroup(\"memory.high\");\n\ - let max = host_budget_tightest_cgroup(\"memory.max\");\n\ - let v1 = match v1_reading {\n\ - Some((d, HostBudgetCgroupV1::Limited(b))) => Some((\n\ - format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", d),\n\ - b,\n\ - )),\n\ - _ => None,\n\ + }\n\n\ + pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n\ + let cgroup_v1_limit = match cgroup_v1_limit {\n\ + Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n\ + return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n\ + }\n\ + Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n\ + Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n\ };\n\ - let observed = [\n\ - high.map(|(d, b)| (format!(\"cgroup memory.high ({})\", d), b)),\n\ - max.map(|(d, b)| (format!(\"cgroup memory.max ({})\", d), b)),\n\ - v1,\n\ - ]\n\ - .into_iter()\n\ - .flatten()\n\ - .min_by_key(|(_, b)| *b);\n\ - if let Some((label, bytes)) = observed {\n\ - let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n\ - let source = match env {\n\ - Some(requested) => format!(\n\ - \"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\",\n\ - effective, requested, label, bytes\n\ - ),\n\ - None => label,\n\ - };\n\ - return (Some(effective), source);\n\ + let observation = [\n\ + cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n\ + cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n\ + cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n\ + ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n\ + if let Some((source, observed_bytes)) = observation {\n\ + return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n\ }\n\ - if let Some(bytes) = host_budget_darwin_physical() {\n\ - let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes);\n\ - let label = \"sysctl hw.memsize\";\n\ - let source = match env {\n\ - Some(requested) => format!(\n\ - \"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\",\n\ - effective, requested, label, bytes\n\ - ),\n\ - None => label.to_string(),\n\ - };\n\ - return (Some(effective), source);\n\ + if let Some(bytes) = darwin_physical {\n\ + return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n\ }\n\ - if let Some(requested) = env {\n\ - return (\n\ - Some(requested),\n\ - format!(\n\ - \"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\",\n\ - requested\n\ - ),\n\ - );\n\ + if let Some(requested_bytes) = env_override {\n\ + return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n\ }\n\ - (\n\ - None,\n\ - format!(\n\ - \"unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\",\n\ - std::env::consts::OS\n\ - ),\n\ - )\n\ + HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n\ + }\n\n\ + pub fn read_host_budget_bytes() -> (Option, String) {\n\ + let join = resolve_host_budget_join(\n\ + std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n\ + host_budget_tightest_cgroup(\"memory.high\"),\n\ + host_budget_tightest_cgroup(\"memory.max\"),\n\ + host_budget_cgroup_v1(),\n\ + host_budget_darwin_physical(),\n\ + );\n\ + (join.bytes(), join.label())\n\ }\n\n\ #[derive(Clone)]\n\ pub enum HostBudgetCgroupV1 {\n\ diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index c1bd689ffd4..f526d6b4b27 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1615,81 +1615,180 @@ pub fn contiguous_loop_elementwise_kernel( out } -/// Host memory planning ceiling as `(bytes, source label)`. -/// Authority: `gunbc.host_budget_source`. Observations are the live reads -/// `read_host_budget_resolution` consumes; this is the `(bytes, label)` view of -/// the same precedence, including cgroup v1 `hierarchical_memory_limit`. -pub fn read_host_budget_bytes() -> (Option, String) { - let env = std::env::var("GUNBC_MEMORY_BUDGET_BYTES") - .ok() - .and_then(|s| s.trim().parse::().ok()); - let v1_reading = host_budget_cgroup_v1(); - if let Some((dir, HostBudgetCgroupV1::Unparseable(body))) = v1_reading.clone() { - return ( - None, - format!( - "unreadable: cgroup v1 memory hierarchy at {dir} holds this process but its \ - hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ - tightest cannot be replaced by another reading" +/// The one host-budget precedence. Authority: `gunbc.host_budget_source`. +/// `read_host_budget_bytes` and `memory_governor::resolve_host_budget` both call this. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HostBudgetJoinSource { + CgroupMemoryHigh { cgroup_dir: String }, + CgroupMemoryMax { cgroup_dir: String }, + CgroupV1HierarchicalMemoryLimit { cgroup_dir: String }, + DarwinPhysicalMemory, +} + +impl HostBudgetJoinSource { + pub fn label(&self) -> String { + match self { + HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => { + format!("cgroup memory.high ({cgroup_dir})") + } + HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => { + format!("cgroup memory.max ({cgroup_dir})") + } + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => { + format!("cgroup v1 memory.stat hierarchical_memory_limit ({cgroup_dir})") + } + HostBudgetJoinSource::DarwinPhysicalMemory => "sysctl hw.memsize".to_string(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HostBudgetJoin { + Resolved { + effective_bytes: u64, + requested_bytes: Option, + source: HostBudgetJoinSource, + observed_bytes: u64, + }, + DeclaredUnverified { + requested_bytes: u64, + reason: String, + }, + Unreadable { + reason: String, + }, +} + +impl HostBudgetJoin { + pub fn bytes(&self) -> Option { + match self { + HostBudgetJoin::Resolved { + effective_bytes, .. + } => Some(*effective_bytes), + HostBudgetJoin::DeclaredUnverified { + requested_bytes, .. + } => Some(*requested_bytes), + HostBudgetJoin::Unreadable { .. } => None, + } + } + + pub fn label(&self) -> String { + match self { + HostBudgetJoin::Resolved { + effective_bytes, + requested_bytes, + source, + observed_bytes, + } => match requested_bytes { + Some(requested) => format!( + "effective planning minimum {effective_bytes} bytes (env request {requested}; observed {}={observed_bytes} bytes)", + source.label() + ), + None => source.label(), + }, + HostBudgetJoin::Unreadable { reason } => format!("unreadable: {reason}"), + HostBudgetJoin::DeclaredUnverified { + requested_bytes, + reason, + } => format!( + "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={requested_bytes}; {reason}" ), - ); + } } - let high = host_budget_tightest_cgroup("memory.high"); - let max = host_budget_tightest_cgroup("memory.max"); - let v1 = match v1_reading { - Some((d, HostBudgetCgroupV1::Limited(b))) => Some(( - format!("cgroup v1 memory.stat hierarchical_memory_limit ({d})"), - b, - )), - _ => None, +} + +pub fn resolve_host_budget_join( + env_override: Option, + cgroup_high: Option<(String, u64)>, + cgroup_max: Option<(String, u64)>, + cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, + darwin_physical: Option, +) -> HostBudgetJoin { + let cgroup_v1_limit = match cgroup_v1_limit { + Some((dir, HostBudgetCgroupV1::Unparseable(body))) => { + return HostBudgetJoin::Unreadable { + reason: format!( + "cgroup v1 memory hierarchy at {dir} holds this process but its \ + hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ + tightest cannot be replaced by another reading" + ), + }; + } + Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)), + Some((_, HostBudgetCgroupV1::Unlimited)) | None => None, }; - let observed = [ - high.map(|(d, b)| (format!("cgroup memory.high ({})", d), b)), - max.map(|(d, b)| (format!("cgroup memory.max ({})", d), b)), - v1, + let observation = [ + cgroup_high + .map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)), + cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)), + cgroup_v1_limit.map(|(cgroup_dir, b)| { + ( + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, + b, + ) + }), ] .into_iter() .flatten() - .min_by_key(|(_, b)| *b); - if let Some((label, bytes)) = observed { - let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes); - let source = match env { - Some(requested) => format!( - "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", - effective, requested, label, bytes - ), - None => label, + .fold( + None::<(HostBudgetJoinSource, u64)>, + |best, cand| match best { + Some(cur) if cur.1 <= cand.1 => Some(cur), + _ => Some(cand), + }, + ); + if let Some((source, observed_bytes)) = observation { + return HostBudgetJoin::Resolved { + effective_bytes: env_override + .map(|requested| requested.min(observed_bytes)) + .unwrap_or(observed_bytes), + requested_bytes: env_override, + source, + observed_bytes, }; - return (Some(effective), source); - } - if let Some(bytes) = host_budget_darwin_physical() { - let effective = env.map(|e| e.min(bytes)).unwrap_or(bytes); - let label = "sysctl hw.memsize"; - let source = match env { - Some(requested) => format!( - "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", - effective, requested, label, bytes - ), - None => label.to_string(), + } + if let Some(bytes) = darwin_physical { + return HostBudgetJoin::Resolved { + effective_bytes: env_override + .map(|requested| requested.min(bytes)) + .unwrap_or(bytes), + requested_bytes: env_override, + source: HostBudgetJoinSource::DarwinPhysicalMemory, + observed_bytes: bytes, }; - return (Some(effective), source); } - if let Some(requested) = env { - return ( - Some(requested), - format!( - "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit", - requested - ), - ); + if let Some(requested_bytes) = env_override { + return HostBudgetJoin::DeclaredUnverified { + requested_bytes, + reason: "no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string(), + }; } - ( - None, - format!( - "unreadable: no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", + HostBudgetJoin::Unreadable { + reason: format!( + "no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES \ + cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than \ + admitting against the widest signal available: a host-shared reading is a number \ + about the MACHINE, not about this slot, and admitting against one is the rc=137 \ + SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, \ + gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must \ + expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", std::env::consts::OS ), - ) + } +} + +/// `(bytes, source label)` view of `resolve_host_budget_join` over the live observations. +pub fn read_host_budget_bytes() -> (Option, String) { + let join = resolve_host_budget_join( + std::env::var("GUNBC_MEMORY_BUDGET_BYTES") + .ok() + .and_then(|s| s.trim().parse::().ok()), + host_budget_tightest_cgroup("memory.high"), + host_budget_tightest_cgroup("memory.max"), + host_budget_cgroup_v1(), + host_budget_darwin_physical(), + ); + (join.bytes(), join.label()) } #[derive(Clone)] From 03ca2f8c7d0ea6d7e78162b7161e14d981950290 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 16:03:07 +0000 Subject: [PATCH 12/27] Name Complete overflow from the host-budget join, and read Darwin via sysctlbyname. Co-authored-by: Cursor --- src/v1/05_emit_rust.dag | 8 ++-- src/v1/runtime_rust.dag | 25 +++++------ src/v1/stage0/src/cli_run/emit_host.rs | 6 +++ src/v1/stage0/src/memory_governor.rs | 26 +---------- src/v1/stage0/src/v1_compiler_emit_rust.rs | 4 +- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 25 +++++------ src/v1/stage0/src/v1_interpreter.rs | 2 +- src/v1/stage0/src/v1_rt.rs | 43 ++++++++++++------- 8 files changed, 61 insertions(+), 78 deletions(-) diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 396768eae07..b195dc7f170 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -17072,17 +17072,17 @@ fn shell_error_stderr_binding(result_fields: List) -> String { fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { if op_has_stderr_capture_param(op_node: op_node, source_indices: source_indices) { concat( - "let __stderr_complete_limit: Option = match &*stderr_capture {\n", + "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", " match v1_rt::read_host_budget_bytes() {\n", - " (Some(n), _) => Some(n as usize),\n", + " (Some(n), source) => (Some(n as usize), source),\n", " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n", " }\n", " }\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n", " let n = crate::std_measure::byte_size_count(bytes.clone());\n", " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n", - " None\n", + " (None, String::new())\n", " }\n", "};\n", "let __stderr_tail_bytes: usize = match &*stderr_capture {\n", @@ -17101,7 +17101,7 @@ fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" +data shell_capture_join_project: String = "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n" // Answers the declared fields in declared order, from the projection the binding // resolved, as the method's SUCCESS value. diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 9d7ab65edd6..bcee5de0326 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -1475,23 +1475,18 @@ fn rt_host_budget() -> String { " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", " \}\n", "\}\n\n", + "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n", "pub fn host_budget_darwin_physical() -> Option \{\n", - " if std::env::consts::OS != \"macos\" \{\n", - " return None;\n", + " #[cfg(target_os = \"macos\")]\n", + " \{\n", + " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n", + " let mut value: u64 = 0;\n", + " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n", + " let rc = unsafe \{ libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) \};\n", + " if rc == 0 && value > 0 \{ Some(value) \} else \{ None \}\n", " \}\n", - " let out = std::process::Command::new(\"sysctl\")\n", - " .args([\"-n\", \"hw.memsize\"])\n", - " .output()\n", - " .ok()?;\n", - " if !out.status.success() \{\n", - " return None;\n", - " \}\n", - " String::from_utf8(out.stdout)\n", - " .ok()?\n", - " .trim()\n", - " .parse::()\n", - " .ok()\n", - " .filter(|v| *v > 0)\n", + " #[cfg(not(target_os = \"macos\"))]\n", + " \{ None \}\n", "\}\n" ) } diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 90a03c4c9ef..225b2427f9a 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3530,6 +3530,7 @@ mod fixture_closure_union_tests { let program = format!( "fn main() -> Result<(), Box> {{\n\ let __stderr_complete_limit: Option = {limit};\n\ + let __stderr_complete_source: String = {source};\n\ let __stderr_tail_bytes: usize = {tail};\n\ let mut output = std::process::Command::new(\"sh\")\n\ .args([\"-c\", \"{script}\"])\n\ @@ -3543,6 +3544,11 @@ mod fixture_closure_union_tests { }}\n", script = script.replace("{payload}", stderr_payload), limit = limit, + source = if complete_limit.is_some() { + "\"host budget\"".to_string() + } else { + "String::new()".to_string() + }, tail = tail_bytes, drain = drain, ); diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index a9e4346f53f..7524a3149ca 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1563,32 +1563,8 @@ pub fn read_cgroup_raw(dir: &Path, file: &str) -> Option { /// /// Exists because the governor previously had NO source on Darwin and fell back to the most /// permissive cap it could name; macOS's memory facts were never asked for. -#[cfg(target_os = "macos")] pub fn darwin_physical_memory_bytes() -> Option { - let name = c"hw.memsize"; - let mut value: u64 = 0; - let mut len: libc::size_t = std::mem::size_of::() as libc::size_t; - // SAFETY: `name` is a NUL-terminated literal, `value`/`len` are live locals sized to - // match, and `newp`/`newlen` are null/0 for a read-only query per sysctl(3). - let rc = unsafe { - libc::sysctlbyname( - name.as_ptr(), - (&mut value as *mut u64).cast::(), - &mut len, - std::ptr::null_mut(), - 0, - ) - }; - if rc == 0 && value > 0 { - Some(value) - } else { - None - } -} - -#[cfg(not(target_os = "macos"))] -pub fn darwin_physical_memory_bytes() -> Option { - None + crate::v1_rt::host_budget_darwin_physical() } /// The bind request an executor may carry, read from `GUNBC_BIND_MEMORY_CGROUP_BYTES`. diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 57c1d313b7b..49c57227ed5 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39223,7 +39223,7 @@ pub fn emit_shell_stderr_policy_binding( source_indices: Rc>>, ) -> String { if op_has_stderr_capture_param(op_node, source_indices) { - "let __stderr_complete_limit: Option = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match v1_rt::read_host_budget_bytes() {\n (Some(n), _) => Some(n as usize),\n (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n None\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() + "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match v1_rt::read_host_budget_bytes() {\n (Some(n), source) => (Some(n as usize), source),\n (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n (None, String::new())\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() } else { v1_rt::concat( v1_rt::concat( @@ -39244,7 +39244,7 @@ pub fn shell_capture_drain_start() -> String { } pub fn shell_capture_join_project() -> String { - "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds GUNBC_MEMORY_BUDGET_BYTES {}\", stderr_total_u64, max_bytes).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() } pub fn shell_argv_param_is_word_list( diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 830a05dd15d..1f3d27d08f7 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -333,23 +333,18 @@ pub fn rt_host_budget() -> String { Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n\ }\n\ }\n\n\ + /// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n\ pub fn host_budget_darwin_physical() -> Option {\n\ - if std::env::consts::OS != \"macos\" {\n\ - return None;\n\ + #[cfg(target_os = \"macos\")]\n\ + {\n\ + let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n\ + let mut value: u64 = 0;\n\ + let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n\ + let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n\ + if rc == 0 && value > 0 { Some(value) } else { None }\n\ }\n\ - let out = std::process::Command::new(\"sysctl\")\n\ - .args([\"-n\", \"hw.memsize\"])\n\ - .output()\n\ - .ok()?;\n\ - if !out.status.success() {\n\ - return None;\n\ - }\n\ - String::from_utf8(out.stdout)\n\ - .ok()?\n\ - .trim()\n\ - .parse::()\n\ - .ok()\n\ - .filter(|v| *v > 0)\n\ + #[cfg(not(target_os = \"macos\"))]\n\ + { None }\n\ }\n" .to_string() } diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 86ef4c49f09..b09175e7e91 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -18069,7 +18069,7 @@ fn dispatch_shell( let (budget, source) = crate::memory_governor::read_host_budget_bytes(); Some(budget.ok_or_else(|| InterpError::TypeError { msg: format!( - "WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active GUNBC_MEMORY_BUDGET_BYTES authority ({source})" + "WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({source})" ), })? as usize) } diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index f526d6b4b27..f2721201add 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1899,21 +1899,32 @@ fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBud } } +/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`. pub fn host_budget_darwin_physical() -> Option { - if std::env::consts::OS != "macos" { - return None; - } - let out = std::process::Command::new("sysctl") - .args(["-n", "hw.memsize"]) - .output() - .ok()?; - if !out.status.success() { - return None; - } - String::from_utf8(out.stdout) - .ok()? - .trim() - .parse::() - .ok() - .filter(|v| *v > 0) + #[cfg(target_os = "macos")] + { + let name = std::ffi::CStr::from_bytes_with_nul(b"hw.memsize\0").ok()?; + let mut value: u64 = 0; + let mut len: libc::size_t = std::mem::size_of::() as libc::size_t; + // SAFETY: `name` is a NUL-terminated literal, `value`/`len` are live locals sized to + // match, and `newp`/`newlen` are null/0 for a read-only query per sysctl(3). + let rc = unsafe { + libc::sysctlbyname( + name.as_ptr(), + (&mut value as *mut u64).cast::(), + &mut len, + std::ptr::null_mut(), + 0, + ) + }; + if rc == 0 && value > 0 { + Some(value) + } else { + None + } + } + #[cfg(not(target_os = "macos"))] + { + None + } } From c314ba24dcd1ae6cbdf6eecafbf5da26db48e847 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 17:34:24 +0000 Subject: [PATCH 13/27] Cite every emit_host capture item and type-check stderr_capture once. The seed-growth row now lists the inline-module path and every new fixture_closure_union_tests item. Emit wall and rust renderer share operation_declares_required_stderr_capture_policy so a String, optional, or collection named stderr_capture refuses as TransportEmissionNotModeled. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 41 ++++++++++---- src/v1/05_emit.dag | 22 ++++++-- src/v1/05_emit_rust.dag | 8 +-- src/v1/stage0/src/cli_run/emit_host.rs | 56 ++++++++++++++++++- src/v1/stage0/src/v1_compiler_emit.rs | 35 +++++++++++- src/v1/stage0/src/v1_compiler_emit_rust.rs | 15 ++--- 6 files changed, 139 insertions(+), 38 deletions(-) diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag index 26771ad6098..40c4018a719 100644 --- a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag +++ b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag @@ -24,20 +24,37 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } // SeedFeatureCompletion FIRES and is admitted on the ruling above. PublicSurfaceGrowth: // the emit_host additions are cfg(test) discriminators, not a published CLI. // -// Hand items below are the emit_host executing harness. The emitter body lives in -// src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. +// Hand items below are the emit_host executing harness. The rust-item authority keeps +// the inline-module path, so every citation is +// v1_compiler.cli_run.emit_host.fixture_closure_union_tests. The emitter body lives +// in src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { hand_authored_declarations: [ - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_bounded_tail_under_cap_is_complete", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_over_budget_refuses", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_complete_under_budget_retains_all", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, - DeclarationRef { module_path: "v1_compiler.cli_run.emit_host", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_MEMBER", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_UNMODELED_SIBLING", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_WITHOUT_POLICY", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_STRING_POLICY", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_OPTIONAL_POLICY", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_COLLECTION_POLICY", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "GUNBC_MODULE_REACH_MEMBER", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "declared_stderr_capture_channels_do_not_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "an_unmodeled_shell_channel_still_refuses_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_without_stderr_capture_input_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_string_stderr_capture_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_optional_stderr_capture_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_collection_stderr_capture_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_bounded_tail_under_cap_is_complete", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_complete_over_budget_refuses", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_complete_under_budget_retains_all", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } ], - reason: "Operator ruling 2026-10-06 (msg_7853a1af): admit SeedFeatureCompletion, exact scope the rust shell handler realizing WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / stderr_retained_bytes) and its emit_host.rs tests. The rust shell handler must realize those channels so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process; a .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", + reason: "Operator ruling 2026-10-06 (msg_7853a1af): admit SeedFeatureCompletion, exact scope the rust shell handler realizing WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / stderr_retained_bytes) and its emit_host.rs tests. The rust shell handler must realize those channels so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process, and they refuse a name-only, String, optional, or collection stderr_capture at TransportEmissionNotModeled. A .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, - trigger: "Delete these items when the v2-native emitter realizes the shell transport's stderr capture, a .dag witness compiles the emitted rust crate and runs the same six discriminators without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", - current_boundary: "Bind and validate WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without the typed policy refuse at the rust emit diagnostic and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." + trigger: "Delete these items when the v2-native emitter realizes the shell transport's stderr capture, a .dag witness compiles the emitted rust crate and runs the same six discriminators plus the typed-policy emit refusals without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", + current_boundary: "Bind and validate a required scalar stderr_capture: WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without that typed required scalar refuse at the rust emit diagnostic (one shared predicate) and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." } diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index d60b9346f31..ec096cad3d3 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -34,7 +34,7 @@ import v1.std.core { is_file_transport, is_local_transport, TransportKind, RestTransport, ShellTransport, FileTransport, LocalTransport, classify_transport, transport_verb, transport_base_path, transport_has_auth, field_init_operation_modifier, operation_modifier_name, with_required_cardinality, tuple_type_name, find_child_named, - Connective, Conj, Disj, NoConnective, Arrow, CardOptional + Connective, Conj, Disj, NoConnective, Arrow, Required, CardOptional } import v1.compiler.infer_env { TypeEnv, TypeBinding, authored_name, lookup_type_for, GlobalBareLookupState, empty_symbol_index } @@ -64,7 +64,8 @@ import v1.compiler.infer_lookup { lookup_func_sig } import v1.compiler.infer { InferScope, is_where_refinement_type, - build_params_scope, extend_scope, call_param_caller_labels + build_params_scope, extend_scope, call_param_caller_labels, + resolved_type_name } import v1.compiler.infer_emit_info { TypeSummary, EmitGraphInfo } @@ -1426,7 +1427,7 @@ fn shell_emission_refusal_fact(refusal: ShellEmissionRefusal) -> String { " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran") ShellCapturePolicyInputAbsent { key: k } => concat("shell output channel '", k, - "' is a capture-accounting channel and this operation declares no stderr_capture input -- answering it would apply a policy nobody declared") + "' is a capture-accounting channel and this operation does not declare a required scalar stderr_capture: WitnessStderrCapturePolicy -- answering it would apply a policy nobody declared") } } @@ -2588,6 +2589,19 @@ fn file_operation_has_content_input(op_node: Node, source_indices: Map) -> Bool { + let ty = param_node_type_expr(n: p) + param_node_name_at(n: p, source_indices: source_indices) == "stderr_capture" + && p.return_cardinality == Required + && ty.return_cardinality == Required + && !node_is_collection(n: ty, source_indices: source_indices) + && qualified_last_segment(name: resolved_type_name(n: p, source_indices: source_indices)) == "WitnessStderrCapturePolicy" +} + +fn operation_declares_required_stderr_capture_policy(op_node: Node, source_indices: Map) -> Bool { + op_node.params |> any(p => param_is_required_stderr_capture_policy(p: p, source_indices: source_indices)) +} + // The channels the interpreter's map_file_outputs answers, read back as the emitter's obligation. // Emission and interpretation are one decision procedure run in two directions (DESIGN §4), so // this list is the emitted side of that map and diverging from it is a behavioral fork. @@ -2829,7 +2843,7 @@ fn unmodeled_shell_transport_operation_diagnostics(tm: TypedModule, item: Node, Present { value: c } => if shell_channel_is_capture_accounting(c: c) && target_is_rust(target: target) - && !file_operation_has_input(op_node: op_node, name: "stderr_capture", source_indices: si) + && !operation_declares_required_stderr_capture_policy(op_node: op_node, source_indices: si) { [make_error_node( diagnostic: TransportEmissionNotModeled { diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index b195dc7f170..d0a813fb7fb 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -268,7 +268,7 @@ import v1.compiler.emit { ShellChanExitSuccess, ShellChanExitCode, ShellChanStdoutLines, ShellChanStderrTruncated, ShellChanStderrTotalBytes, ShellChanStderrRetainedBytes, ShellChannelNotRealizedByTarget, ShellCapturePolicyInputAbsent, shell_emission_refusal_fact, shell_result_channel_key, - shell_channel_is_capture_accounting, + shell_channel_is_capture_accounting, operation_declares_required_stderr_capture_policy, bind_operation_transport, transport_binding_refusal_fact, FileVerb, FileRead, FileWrite, FileWriteOwnerOnly, FileWriteCreateNew, FileWriteCreateNewWithMode, FileLinkCreateNew, FileDelete, FileList, FileResultField, FileResultChannel, FileChanSuccess, FileChanByteCount, @@ -17061,16 +17061,12 @@ fn shell_needs_capture_accounting(result_fields: List) -> Bool result_fields |> any(f => shell_channel_is_capture_accounting(c: f.channel)) } -fn op_has_stderr_capture_param(op_node: Node, source_indices: Map) -> Bool { - op_node.params |> any(p => param_node_name_at(n: p, source_indices: source_indices) == "stderr_capture") -} - fn shell_error_stderr_binding(result_fields: List) -> String { if shell_needs_capture_accounting(result_fields: result_fields) { "" } else { concat(shell_stderr_binding_line, " ") } } fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { - if op_has_stderr_capture_param(op_node: op_node, source_indices: source_indices) { + if operation_declares_required_stderr_capture_policy(op_node: op_node, source_indices: source_indices) { concat( "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 225b2427f9a..195d5704947 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3443,6 +3443,12 @@ mod fixture_closure_union_tests { const STDERR_CAPTURE_WITHOUT_POLICY: &str = "module efr_member\nservice Bin {\n operation Run {\n input { bin_path: String }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const STDERR_CAPTURE_STRING_POLICY: &str = "module efr_member\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: String\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_OPTIONAL_POLICY: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy?\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + + const STDERR_CAPTURE_COLLECTION_POLICY: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: List\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const GUNBC_MODULE_REACH_MEMBER: &str = "module efr_member\nimport extdeps.gunbc { packages }\nfn ignore() -> Int { 0 }\n"; @@ -3475,11 +3481,57 @@ mod fixture_closure_union_tests { .expect_err("a capture channel without stderr_capture must refuse"); assert!( refusal.contains("cause=FixtureClosureUnionEmitRefused") - && refusal.contains("stderr_capture"), + && refusal.contains("stderr_capture") + && refusal.contains("transport emission is not modeled"), "{refusal}" ); } + fn capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + source: &str, + label: &str, + ) { + let union = fixture_closure_union_control_union(source).unwrap_or_else(|e| { + panic!("{label} must resolve so emit can refuse the typed policy: {e}") + }); + let refusal = fixture_closure_union_emit_receipt(&union) + .expect_err("wrong stderr_capture shape must refuse at emit, not rustc"); + assert!( + refusal.contains("cause=FixtureClosureUnionEmitRefused") + && refusal.contains("transport emission is not modeled") + && refusal.contains("stderr_capture"), + "{label}: {refusal}" + ); + assert!( + !refusal.contains("error[E") && !refusal.contains("mismatched types"), + "typed emission must refuse before rustc: {label}: {refusal}" + ); + } + + #[test] + fn capture_channels_with_string_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_STRING_POLICY, + "String", + ); + } + + #[test] + fn capture_channels_with_optional_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_OPTIONAL_POLICY, + "optional", + ); + } + + #[test] + fn capture_channels_with_collection_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_COLLECTION_POLICY, + "collection", + ); + } + fn rustc_and_run_emitted_capture( stem: &str, complete_limit: Option, @@ -3545,7 +3597,7 @@ mod fixture_closure_union_tests { script = script.replace("{payload}", stderr_payload), limit = limit, source = if complete_limit.is_some() { - "\"host budget\"".to_string() + "\"host budget\".to_string()".to_string() } else { "String::new()".to_string() }, diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index f3e34395019..34b8286af74 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -2928,7 +2928,7 @@ pub fn shell_emission_refusal_fact(refusal: Rc) -> String match (*refusal.clone()).clone() { ShellEmissionRefusal::ShellOutputKeyNotModeled { key: k, .. } => v1_rt::concat(v1_rt::concat("shell transport output key '".to_string(), k.clone()), "' has no modeled channel -- the modeled channels are stdout, stderr, exit_success, success, exists, exit_code, stdout_lines, stderr_truncated, stderr_total_bytes and stderr_retained_bytes".to_string()), ShellEmissionRefusal::ShellChannelNotRealizedByTarget { key: k, target_name: tn, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a modeled channel that target ".to_string()), tn.clone()), " cannot realize -- the emitted realization implements no stderr capture policy, so answering it would assert that the declared policy ran".to_string()), - ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: k, .. } => v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a capture-accounting channel and this operation declares no stderr_capture input -- answering it would apply a policy nobody declared".to_string()), + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { key: k, .. } => v1_rt::concat(v1_rt::concat("shell output channel '".to_string(), k.clone()), "' is a capture-accounting channel and this operation does not declare a required scalar stderr_capture: WitnessStderrCapturePolicy -- answering it would apply a policy nobody declared".to_string()), } } @@ -5395,6 +5395,36 @@ pub fn file_transport_declared_verb( } } +pub fn param_is_required_stderr_capture_policy( + p: Rc, + source_indices: Rc>>, +) -> bool { + let ty = crate::v1_std_core::param_node_type_expr(p.clone()); + (crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) == "stderr_capture") + && (p.return_cardinality.clone() == crate::v1_std_core::Cardinality::Required) + && (ty.return_cardinality.clone() == crate::v1_std_core::Cardinality::Required) + && (!crate::v1_compiler_infer_types::node_is_collection(ty.clone(), source_indices.clone())) + && (crate::v1_std_core::qualified_last_segment( + crate::v1_compiler_infer::resolved_type_name(p.clone(), source_indices.clone()), + ) == "WitnessStderrCapturePolicy") +} + +pub fn operation_declares_required_stderr_capture_policy( + op_node: Rc, + source_indices: Rc>>, +) -> bool { + { + let mut __found = false; + for p in op_node.params.clone().iter().cloned() { + if param_is_required_stderr_capture_policy(p.clone(), source_indices.clone()) { + __found = true; + break; + } + } + __found + } +} + pub fn file_operation_has_input( op_node: Rc, name: String, @@ -5752,9 +5782,8 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { }, Some(c) => if shell_channel_is_capture_accounting(c.clone()) && target_is_rust(target.clone()) - && !file_operation_has_input( + && !operation_declares_required_stderr_capture_policy( op_node.clone(), - "stderr_capture".to_string(), si.clone(), ) { diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 49c57227ed5..c708239cdef 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -39200,16 +39200,6 @@ pub fn shell_needs_capture_accounting(result_fields: Rc .any(|f| crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone())) } -pub fn op_has_stderr_capture_param( - op_node: Rc, - source_indices: Rc>>, -) -> bool { - op_node.params.iter().any(|p| { - crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) - == "stderr_capture" - }) -} - pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { if shell_needs_capture_accounting(result_fields) { "".to_string() @@ -39222,7 +39212,10 @@ pub fn emit_shell_stderr_policy_binding( op_node: Rc, source_indices: Rc>>, ) -> String { - if op_has_stderr_capture_param(op_node, source_indices) { + if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( + op_node, + source_indices, + ) { "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match v1_rt::read_host_budget_bytes() {\n (Some(n), source) => (Some(n as usize), source),\n (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n (None, String::new())\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() } else { v1_rt::concat( From d2a817c00d3f34e881ef72e0184019585bc5d501 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 19:20:48 +0000 Subject: [PATCH 14/27] Install the seed-emitted stage0 mirrors for the typed capture wall. The generated job refused because the committed emit, emit_rust, runtime_rust and v1_rt mirrors were not one seed emission of the current .dag sources. Co-authored-by: Cursor --- src/v1/stage0/src/v1_compiler_emit.rs | 97 ++--- src/v1/stage0/src/v1_compiler_emit_rust.rs | 376 +++++++++--------- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 215 +--------- src/v1/stage0/src/v1_rt.rs | 58 +-- 4 files changed, 286 insertions(+), 460 deletions(-) diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 34b8286af74..bd0dd3c6b55 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -55,6 +55,7 @@ pub use crate::v1_compiler_emit_core_support::{EmitResult, TestProjection}; pub use crate::v1_compiler_infer::InferScope; pub use crate::v1_compiler_infer::{ build_params_scope, call_param_caller_labels, extend_scope, is_where_refinement_type, + resolved_type_name, }; use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::{ DataVariantBareString, DataVariantInternalTagged, DataVariantSpellingRefused, @@ -110,7 +111,7 @@ use crate::v1_std_core::CallSemantics::ResolvedDirectCallSemantics; use crate::v1_std_core::CallTargetIdentity::{ CallableTargetUndetermined, LocallyBoundCall, RuntimePrimitiveCall, SourceDeclarationCall, }; -use crate::v1_std_core::Cardinality::CardOptional; +use crate::v1_std_core::Cardinality::{CardOptional, Required}; use crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled; use crate::v1_std_core::Connective::{Arrow, Conj, Disj, NoConnective}; use crate::v1_std_core::ExprData::{ @@ -2894,12 +2895,12 @@ pub fn shell_result_channel_key(c: ShellResultChannel) -> String { } pub fn shell_channel_is_capture_accounting(c: ShellResultChannel) -> bool { - matches!( - c, - ShellResultChannel::ShellChanStderrTruncated - | ShellResultChannel::ShellChanStderrTotalBytes - | ShellResultChannel::ShellChanStderrRetainedBytes - ) + match c.clone() { + ShellResultChannel::ShellChanStderrTruncated => true, + ShellResultChannel::ShellChanStderrTotalBytes => true, + ShellResultChannel::ShellChanStderrRetainedBytes => true, + _ => false, + } } pub fn shell_channel_realized_by_target(c: ShellResultChannel, target: RenderTarget) -> bool { @@ -5395,36 +5396,6 @@ pub fn file_transport_declared_verb( } } -pub fn param_is_required_stderr_capture_policy( - p: Rc, - source_indices: Rc>>, -) -> bool { - let ty = crate::v1_std_core::param_node_type_expr(p.clone()); - (crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) == "stderr_capture") - && (p.return_cardinality.clone() == crate::v1_std_core::Cardinality::Required) - && (ty.return_cardinality.clone() == crate::v1_std_core::Cardinality::Required) - && (!crate::v1_compiler_infer_types::node_is_collection(ty.clone(), source_indices.clone())) - && (crate::v1_std_core::qualified_last_segment( - crate::v1_compiler_infer::resolved_type_name(p.clone(), source_indices.clone()), - ) == "WitnessStderrCapturePolicy") -} - -pub fn operation_declares_required_stderr_capture_policy( - op_node: Rc, - source_indices: Rc>>, -) -> bool { - { - let mut __found = false; - for p in op_node.params.clone().iter().cloned() { - if param_is_required_stderr_capture_policy(p.clone(), source_indices.clone()) { - __found = true; - break; - } - } - __found - } -} - pub fn file_operation_has_input( op_node: Rc, name: String, @@ -5455,6 +5426,42 @@ pub fn file_operation_has_content_input( ) } +pub fn param_is_required_stderr_capture_policy( + p: Rc, + source_indices: Rc>>, +) -> bool { + { + let ty = crate::v1_std_core::param_node_type_expr(p.clone()); + (((((crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) + == "stderr_capture".to_string()) + && (p.return_cardinality.clone() == Cardinality::Required)) + && (ty.return_cardinality.clone() == Cardinality::Required)) + && !crate::v1_compiler_infer_types::node_is_collection( + ty.clone(), + source_indices.clone(), + )) + && (crate::v1_std_core::qualified_last_segment( + crate::v1_compiler_infer::resolved_type_name(p.clone(), source_indices.clone()), + ) == "WitnessStderrCapturePolicy".to_string())) + } +} + +pub fn operation_declares_required_stderr_capture_policy( + op_node: Rc, + source_indices: Rc>>, +) -> bool { + { + let mut __found = false; + for p in op_node.params.clone().iter().cloned() { + if param_is_required_stderr_capture_policy(p.clone(), source_indices.clone()) { + __found = true; + break; + } + } + __found + } +} + pub fn is_modeled_file_output_channel(key: String) -> bool { match file_result_channel_of_key(key.clone()) { Some(_) => true, @@ -5780,13 +5787,7 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { span: ch.span.clone(), }), module_name.clone())]) }, - Some(c) => if shell_channel_is_capture_accounting(c.clone()) - && target_is_rust(target.clone()) - && !operation_declares_required_stderr_capture_policy( - op_node.clone(), - si.clone(), - ) - { + Some(c) => if ((shell_channel_is_capture_accounting(c.clone()) && target_is_rust(target.clone())) && !operation_declares_required_stderr_capture_policy(op_node.clone(), si.clone())) { Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { transport_kind: "shell".to_string(), service: service_name.clone(), @@ -5798,10 +5799,11 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { })), span: ch.span.clone(), }), module_name.clone())]) - } else if shell_channel_realized_by_target(c.clone(), target.clone()) { - Rc::new(vec![]) } else { - Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { + if shell_channel_realized_by_target(c.clone(), target.clone()) { + Rc::new(vec![]) + } else { + Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { transport_kind: "shell".to_string(), service: service_name.clone(), operation: crate::v1_compiler_infer_env::authored_name(env.clone(), op_node.clone()), @@ -5813,6 +5815,7 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { })), span: ch.span.clone(), }), module_name.clone())]) + } }, }).iter().cloned()); } __result }), _ => Rc::new(vec![]), diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index c708239cdef..571a14b2922 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -178,7 +178,9 @@ use crate::v1_compiler_emit::FileVerb::{ FileDelete, FileLinkCreateNew, FileList, FileRead, FileWrite, FileWriteCreateNew, FileWriteCreateNewWithMode, FileWriteOwnerOnly, }; -use crate::v1_compiler_emit::ShellEmissionRefusal::ShellChannelNotRealizedByTarget; +use crate::v1_compiler_emit::ShellEmissionRefusal::{ + ShellCapturePolicyInputAbsent, ShellChannelNotRealizedByTarget, +}; use crate::v1_compiler_emit::ShellResultChannel::{ ShellChanExitCode, ShellChanExitSuccess, ShellChanStderr, ShellChanStderrRetainedBytes, ShellChanStderrTotalBytes, ShellChanStderrTruncated, ShellChanStdout, ShellChanStdoutLines, @@ -193,10 +195,11 @@ pub use crate::v1_compiler_emit::{ emit_typed_if_shared, emit_typed_let_shared, emit_unary_op, escape_rust_interp_text, extract_modifier_names, has_nested_records_node, has_service_items, is_null_coalesce, is_self_recursive, is_tco_eligible, keyed_container_has_target_inhabitant, - lookup_item_by_identity, module_emit_scope, order_typed_call_args_from_semantics, - render_node_type, render_tuple_parts, rust_literal_for_pattern, scope_after_expr, - seed_bindings, service_fallback_transport, service_field_ctors, service_field_decls, - shared_tco_reassign, shell_emission_refusal_fact, shell_result_channel_key, + lookup_item_by_identity, module_emit_scope, operation_declares_required_stderr_capture_policy, + order_typed_call_args_from_semantics, render_node_type, render_tuple_parts, + rust_literal_for_pattern, scope_after_expr, seed_bindings, service_fallback_transport, + service_field_ctors, service_field_decls, shared_tco_reassign, + shell_channel_is_capture_accounting, shell_emission_refusal_fact, shell_result_channel_key, tco_loop_iteration_lets, tco_loop_slot_name, tco_reassign_core, transport_binding_refusal_fact, }; pub use crate::v1_compiler_emit::{ @@ -38935,7 +38938,7 @@ pub fn emit_shell_call( std::option::Option::None => false, }; let needs_capture = shell_needs_capture_accounting(result_fields.clone()); - let let_kw = if has_stdin.clone() || needs_capture.clone() { + let let_kw = if (has_stdin.clone() || needs_capture.clone()) { "let mut output".to_string() } else { "let output".to_string() @@ -39060,186 +39063,136 @@ pub fn emit_shell_call( source_indices.clone(), ); if needs_capture.clone() { - let policy_bind = - emit_shell_stderr_policy_binding(op_node.clone(), source_indices.clone()); - let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); - let spawn_exec = " .spawn()?;".to_string(); - let stdin_thread = if has_stdin.clone() { - let stdin_expr = - crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) - .clone() - .unwrap(); - let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { - ExprData::ExprVar { - binding_kind: _, .. - } => crate::v1_compiler_emit::emit_ident( - crate::v1_std_core::expr_var_name_at( - stdin_expr.clone(), - source_indices.clone(), - ), - RenderTarget::Rust, - ), - _ => crate::v1_compiler_emit::emit_simple_expr( - stdin_expr.clone(), - RenderTarget::Rust, - source_indices.clone(), - ), - }; - v1_rt::concat( - v1_rt::concat( - "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(".to_string(), - stdin_var.clone(), - ), - ".as_bytes())?;\n }\n Ok(())\n});\n".to_string(), - ) - } else { - "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string() - }; - let capture_lines = v1_rt::concat( - v1_rt::concat(shell_capture_drain_start(), stdin_thread), - shell_capture_join_project(), - ); - let all_lines = v1_rt::concat( - v1_rt::concat( - v1_rt::concat( - Rc::new(vec![policy_bind, cmd_line.clone()]), - arg_lines.clone(), - ), - env_lines.clone(), - ), - Rc::new(vec![ - wd_line.clone(), - spawn_line, - spawn_exec, - capture_lines, - return_line.clone(), - ]), - ); - all_lines.join(&"\n".to_string()) - } else if has_stdin.clone() { { - let stdin_expr = - crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) + let policy_bind = + emit_shell_stderr_policy_binding(op_node.clone(), source_indices.clone()); + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let stdin_thread = if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) .clone() .unwrap(); - let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { - ExprData::ExprVar { - binding_kind: _, .. - } => crate::v1_compiler_emit::emit_ident( - crate::v1_std_core::expr_var_name_at( - stdin_expr.clone(), - source_indices.clone(), - ), - RenderTarget::Rust, - ), - _ => crate::v1_compiler_emit::emit_simple_expr( - stdin_expr.clone(), - RenderTarget::Rust, - source_indices.clone(), - ), + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + v1_rt::concat(v1_rt::concat("let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n Ok(())\n});\n".to_string()) + } + } else { + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string() }; - let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); - let spawn_exec = " .spawn()?;".to_string(); - let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); - let wait_line = "let output = output.wait_with_output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), + let capture_lines = v1_rt::concat( + v1_rt::concat(shell_capture_drain_start(), stdin_thread.clone()), + shell_capture_join_project(), ); - let all_lines = v1_rt::concat( + v1_rt::concat( v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + v1_rt::concat( + Rc::new(vec![policy_bind.clone(), cmd_line.clone()]), + arg_lines.clone(), + ), env_lines.clone(), ), Rc::new(vec![ wd_line.clone(), spawn_line.clone(), spawn_exec.clone(), - write_block.clone(), - wait_line.clone(), - check_line.clone(), + capture_lines.clone(), return_line.clone(), ]), - ); - all_lines.clone().join(&"\n".to_string()) + ) + .join(&"\n".to_string()) } } else { - { - let output_line = " .output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), - ); - let all_lines = v1_rt::concat( + if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) + .clone() + .unwrap(); + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); + let wait_line = "let output = output.wait_with_output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), - env_lines.clone(), - ), - Rc::new(vec![ - wd_line.clone(), - output_line.clone(), - check_line.clone(), - return_line.clone(), - ]), - ); - all_lines.clone().join(&"\n".to_string()) + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + spawn_line.clone(), + spawn_exec.clone(), + write_block.clone(), + wait_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } + } else { + { + let output_line = " .output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + output_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } } } } } -pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { - result_fields - .iter() - .any(|f| crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone())) -} - -pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { - if shell_needs_capture_accounting(result_fields) { - "".to_string() - } else { - v1_rt::concat(shell_stderr_binding_line(), " ".to_string()) - } -} - -pub fn emit_shell_stderr_policy_binding( - op_node: Rc, - source_indices: Rc>>, -) -> String { - if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( - op_node, - source_indices, - ) { - "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n match v1_rt::read_host_budget_bytes() {\n (Some(n), source) => (Some(n as usize), source),\n (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n }\n }\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n let n = crate::std_measure::byte_size_count(bytes.clone());\n if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n (None, String::new())\n }\n};\nlet __stderr_tail_bytes: usize = match &*stderr_capture {\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n};\n".to_string() - } else { - v1_rt::concat( - v1_rt::concat( - "return Err(\"".to_string(), - crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( - crate::v1_compiler_emit::ShellEmissionRefusal::ShellCapturePolicyInputAbsent { - key: "stderr_truncated".to_string(), - }, - )), - ), - "\".into());\n".to_string(), - ) - } -} - -pub fn shell_capture_drain_start() -> String { - "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n".to_string() -} - -pub fn shell_capture_join_project() -> String { - "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() -} - pub fn shell_argv_param_is_word_list( param: Rc, source_indices: Rc>>, @@ -39418,6 +39371,69 @@ pub fn shell_stderr_binding_line() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { + { + let mut __found = false; + for f in result_fields.iter().cloned() { + if crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone()) { + __found = true; + break; + } + } + __found + } +} + +pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { + v1_rt::concat(shell_stderr_binding_line(), " ".to_string()) + } +} + +pub fn emit_shell_stderr_policy_binding( + op_node: Rc, + source_indices: Rc>>, +) -> String { + if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( + op_node.clone(), + source_indices.clone(), + ) { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n".to_string(), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n".to_string()), " match v1_rt::read_host_budget_bytes() {\n".to_string()), " (Some(n), source) => (Some(n as usize), source),\n".to_string()), " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n".to_string()), " }\n".to_string()), " }\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n".to_string()), " let n = crate::std_measure::byte_size_count(bytes.clone());\n".to_string()), " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n".to_string()), " (None, String::new())\n".to_string()), " }\n".to_string()), "};\n".to_string()), "let __stderr_tail_bytes: usize = match &*stderr_capture {\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n".to_string()), "};\n".to_string()) + } else { + v1_rt::concat( + v1_rt::concat( + "return Err(\"".to_string(), + crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: "stderr_truncated".to_string(), + }, + )), + ), + "\".into());\n".to_string(), + ) + } +} + +pub fn shell_capture_drain_start() -> String { + thread_local! { + static CACHED: String = { + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn shell_capture_join_project() -> String { + thread_local! { + static CACHED: String = { + "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn emit_shell_return(result_fields: Rc>>) -> String { v1_rt::concat( v1_rt::concat( @@ -39470,23 +39486,25 @@ pub fn shell_stderr_prelude(result_fields: Rc>>) -> Str if shell_needs_capture_accounting(result_fields.clone()) { "".to_string() } else { - let needs_stderr = { - let mut __found = false; - for f in result_fields.iter().cloned() { - if match f.channel.clone() { - ShellResultChannel::ShellChanStderr => true, - _ => false, - } { - __found = true; - break; + { + let needs_stderr = { + let mut __found = false; + for f in result_fields.iter().cloned() { + if match f.channel.clone() { + ShellResultChannel::ShellChanStderr => true, + _ => false, + } { + __found = true; + break; + } } + __found + }; + if needs_stderr.clone() { + v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) + } else { + "".to_string() } - __found - }; - if needs_stderr { - v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) - } else { - "".to_string() } } } diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 1f3d27d08f7..4c09296fbe6 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -135,218 +135,45 @@ pub fn rust_runtime_source() -> String { v1_rt::concat( v1_rt::concat( v1_rt::concat( - rt_header(), - rt_text_lookup_work_counter(), + v1_rt::concat( + rt_header(), + rt_text_lookup_work_counter(), + ), + rt_resolution_silent_pick_telemetry( + ), ), - rt_resolution_silent_pick_telemetry(), + rt_concat_trait(), ), - rt_concat_trait(), + rt_string_ops(), ), - rt_string_ops(), + rt_string_carrier(), ), - rt_string_carrier(), + rt_collection_ops(), ), - rt_collection_ops(), + rt_rc_container_ops(), ), - rt_rc_container_ops(), + rt_scanner_ops(), ), - rt_scanner_ops(), + rt_unicode_ops(), ), - rt_unicode_ops(), + rt_freemonoid_host_ops(), ), - rt_freemonoid_host_ops(), + rt_hash_ops(), ), - rt_hash_ops(), + rt_filesystem(), ), - rt_filesystem(), + rt_checked_int_ops(), ), - rt_checked_int_ops(), + rt_realization_measurement(), ), - rt_realization_measurement(), + rt_accelerator_demo_kernel(), ), - v1_rt::concat(rt_accelerator_demo_kernel(), rt_host_budget()), + rt_host_budget(), ) } pub fn rt_host_budget() -> String { - "#[derive(Clone, Debug, PartialEq, Eq)]\n\ - pub enum HostBudgetJoinSource {\n\ - CgroupMemoryHigh { cgroup_dir: String },\n\ - CgroupMemoryMax { cgroup_dir: String },\n\ - CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n\ - DarwinPhysicalMemory,\n\ - }\n\n\ - impl HostBudgetJoinSource {\n\ - pub fn label(&self) -> String {\n\ - match self {\n\ - HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n\ - HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n\ - HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n\ - HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n\ - }\n\ - }\n\ - }\n\n\ - #[derive(Clone, Debug, PartialEq, Eq)]\n\ - pub enum HostBudgetJoin {\n\ - Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n\ - DeclaredUnverified { requested_bytes: u64, reason: String },\n\ - Unreadable { reason: String },\n\ - }\n\n\ - impl HostBudgetJoin {\n\ - pub fn bytes(&self) -> Option {\n\ - match self {\n\ - HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n\ - HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n\ - HostBudgetJoin::Unreadable { .. } => None,\n\ - }\n\ - }\n\ - pub fn label(&self) -> String {\n\ - match self {\n\ - HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n\ - Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n\ - None => source.label(),\n\ - },\n\ - HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n\ - HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n\ - }\n\ - }\n\ - }\n\n\ - pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n\ - let cgroup_v1_limit = match cgroup_v1_limit {\n\ - Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n\ - return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n\ - }\n\ - Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n\ - Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n\ - };\n\ - let observation = [\n\ - cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n\ - cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n\ - cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n\ - ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n\ - if let Some((source, observed_bytes)) = observation {\n\ - return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n\ - }\n\ - if let Some(bytes) = darwin_physical {\n\ - return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n\ - }\n\ - if let Some(requested_bytes) = env_override {\n\ - return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n\ - }\n\ - HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n\ - }\n\n\ - pub fn read_host_budget_bytes() -> (Option, String) {\n\ - let join = resolve_host_budget_join(\n\ - std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n\ - host_budget_tightest_cgroup(\"memory.high\"),\n\ - host_budget_tightest_cgroup(\"memory.max\"),\n\ - host_budget_cgroup_v1(),\n\ - host_budget_darwin_physical(),\n\ - );\n\ - (join.bytes(), join.label())\n\ - }\n\n\ - #[derive(Clone)]\n\ - pub enum HostBudgetCgroupV1 {\n\ - Limited(u64),\n\ - Unlimited,\n\ - Unparseable(String),\n\ - }\n\n\ - pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n\ - let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n\ - let rel = self_cg\n\ - .lines()\n\ - .find_map(|l| l.strip_prefix(\"0::\"))\n\ - .map(|p| p.trim().trim_start_matches('/').to_string())?;\n\ - let root = std::path::Path::new(\"/sys/fs/cgroup\");\n\ - let mut dir = root.join(&rel);\n\ - let mut best: Option<(u64, std::path::PathBuf)> = None;\n\ - loop {\n\ - if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n\ - let s = s.trim();\n\ - if s != \"max\" {\n\ - if let Ok(v) = s.parse::() {\n\ - let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n\ - if take {\n\ - best = Some((v, dir.clone()));\n\ - }\n\ - }\n\ - }\n\ - }\n\ - if dir == root || !dir.pop() {\n\ - break;\n\ - }\n\ - }\n\ - best.map(|(v, d)| (d.display().to_string(), v))\n\ - }\n\n\ - pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n\ - let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n\ - let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n\ - let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n\ - if page_size <= 0 {\n\ - return Some((\n\ - \"/proc/self/mountinfo\".to_string(),\n\ - HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n\ - ));\n\ - }\n\ - let page_size = page_size as u64;\n\ - let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n\ - let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) {\n\ - Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n\ - Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n\ - };\n\ - Some((dir, value))\n\ - }\n\n\ - fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n\ - let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n\ - let fields: Vec<&str> = line.split(' ').collect();\n\ - let dash = fields.iter().position(|f| *f == \"-\")?;\n\ - let fstype = fields.get(dash + 1)?;\n\ - let super_opts = fields.get(dash + 3)?;\n\ - if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n\ - Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n\ - } else {\n\ - None\n\ - }\n\ - })?;\n\ - let path = self_cg.lines().find_map(|l| {\n\ - let mut parts = l.splitn(3, ':');\n\ - let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n\ - controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n\ - })?;\n\ - let rel = if mount_root == \"/\" {\n\ - path.as_str()\n\ - } else {\n\ - let rest = path.strip_prefix(mount_root.as_str())?;\n\ - if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n\ - rest\n\ - };\n\ - Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n\ - }\n\n\ - fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n\ - let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n\ - let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n\ - let unlimited = (i64::MAX as u64 / page_size) * page_size;\n\ - match body.trim().parse::() {\n\ - Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n\ - Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n\ - Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n\ - Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n\ - }\n\ - }\n\n\ - /// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n\ - pub fn host_budget_darwin_physical() -> Option {\n\ - #[cfg(target_os = \"macos\")]\n\ - {\n\ - let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n\ - let mut value: u64 = 0;\n\ - let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n\ - let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n\ - if rc == 0 && value > 0 { Some(value) } else { None }\n\ - }\n\ - #[cfg(not(target_os = \"macos\"))]\n\ - { None }\n\ - }\n" - .to_string() + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The one host-budget precedence. `read_host_budget_bytes` and\n".to_string(), "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoinSource {\n".to_string()), " CgroupMemoryHigh { cgroup_dir: String },\n".to_string()), " CgroupMemoryMax { cgroup_dir: String },\n".to_string()), " CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n".to_string()), " DarwinPhysicalMemory,\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoinSource {\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoin {\n".to_string()), " Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n".to_string()), " DeclaredUnverified { requested_bytes: u64, reason: String },\n".to_string()), " Unreadable { reason: String },\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoin {\n".to_string()), " pub fn bytes(&self) -> Option {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n".to_string()), " HostBudgetJoin::Unreadable { .. } => None,\n".to_string()), " }\n".to_string()), " }\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n".to_string()), " Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n".to_string()), " None => source.label(),\n".to_string()), " },\n".to_string()), " HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n".to_string()), " let cgroup_v1_limit = match cgroup_v1_limit {\n".to_string()), " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n".to_string()), " return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n".to_string()), " }\n".to_string()), " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n".to_string()), " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n".to_string()), " };\n".to_string()), " let observation = [\n".to_string()), " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n".to_string()), " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n".to_string()), " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n".to_string()), " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n".to_string()), " if let Some((source, observed_bytes)) = observation {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n".to_string()), " }\n".to_string()), " if let Some(bytes) = darwin_physical {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n".to_string()), " }\n".to_string()), " if let Some(requested_bytes) = env_override {\n".to_string()), " return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n".to_string()), " }\n".to_string()), " HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n".to_string()), "}\n\n".to_string()), "pub fn read_host_budget_bytes() -> (Option, String) {\n".to_string()), " let join = resolve_host_budget_join(\n".to_string()), " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n".to_string()), " host_budget_tightest_cgroup(\"memory.high\"),\n".to_string()), " host_budget_tightest_cgroup(\"memory.max\"),\n".to_string()), " host_budget_cgroup_v1(),\n".to_string()), " host_budget_darwin_physical(),\n".to_string()), " );\n".to_string()), " (join.bytes(), join.label())\n".to_string()), "}\n\n".to_string()), "#[derive(Clone)]\n".to_string()), "pub enum HostBudgetCgroupV1 {\n".to_string()), " Limited(u64),\n".to_string()), " Unlimited,\n".to_string()), " Unparseable(String),\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let rel = self_cg\n".to_string()), " .lines()\n".to_string()), " .find_map(|l| l.strip_prefix(\"0::\"))\n".to_string()), " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n".to_string()), " let root = std::path::Path::new(\"/sys/fs/cgroup\");\n".to_string()), " let mut dir = root.join(&rel);\n".to_string()), " let mut best: Option<(u64, std::path::PathBuf)> = None;\n".to_string()), " loop {\n".to_string()), " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n".to_string()), " let s = s.trim();\n".to_string()), " if s != \"max\" {\n".to_string()), " if let Ok(v) = s.parse::() {\n".to_string()), " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n".to_string()), " if take {\n".to_string()), " best = Some((v, dir.clone()));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " if dir == root || !dir.pop() {\n".to_string()), " break;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " best.map(|(v, d)| (d.display().to_string(), v))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n".to_string()), " let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n".to_string()), " if page_size <= 0 {\n".to_string()), " return Some((\n".to_string()), " \"/proc/self/mountinfo\".to_string(),\n".to_string()), " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n".to_string()), " ));\n".to_string()), " }\n".to_string()), " let page_size = page_size as u64;\n".to_string()), " let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n".to_string()), " let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) {\n".to_string()), " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n".to_string()), " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n".to_string()), " };\n".to_string()), " Some((dir, value))\n".to_string()), "}\n\n".to_string()), "fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n".to_string()), " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n".to_string()), " let fields: Vec<&str> = line.split(' ').collect();\n".to_string()), " let dash = fields.iter().position(|f| *f == \"-\")?;\n".to_string()), " let fstype = fields.get(dash + 1)?;\n".to_string()), " let super_opts = fields.get(dash + 3)?;\n".to_string()), " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n".to_string()), " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n".to_string()), " } else {\n".to_string()), " None\n".to_string()), " }\n".to_string()), " })?;\n".to_string()), " let path = self_cg.lines().find_map(|l| {\n".to_string()), " let mut parts = l.splitn(3, ':');\n".to_string()), " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n".to_string()), " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n".to_string()), " })?;\n".to_string()), " let rel = if mount_root == \"/\" {\n".to_string()), " path.as_str()\n".to_string()), " } else {\n".to_string()), " let rest = path.strip_prefix(mount_root.as_str())?;\n".to_string()), " if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n".to_string()), " rest\n".to_string()), " };\n".to_string()), " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n".to_string()), "}\n\n".to_string()), "fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n".to_string()), " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n".to_string()), " let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n".to_string()), " let unlimited = (i64::MAX as u64 / page_size) * page_size;\n".to_string()), " match body.trim().parse::() {\n".to_string()), " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n".to_string()), " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n".to_string()), " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n".to_string()), "pub fn host_budget_darwin_physical() -> Option {\n".to_string()), " #[cfg(target_os = \"macos\")]\n".to_string()), " {\n".to_string()), " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n".to_string()), " let mut value: u64 = 0;\n".to_string()), " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n".to_string()), " let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n".to_string()), " if rc == 0 && value > 0 { Some(value) } else { None }\n".to_string()), " }\n".to_string()), " #[cfg(not(target_os = \"macos\"))]\n".to_string()), " { None }\n".to_string()), "}\n".to_string()) } pub fn rt_accelerator_demo_kernel() -> String { diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index f2721201add..334a7bf9e36 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1614,9 +1614,6 @@ pub fn contiguous_loop_elementwise_kernel( } out } - -/// The one host-budget precedence. Authority: `gunbc.host_budget_source`. -/// `read_host_budget_bytes` and `memory_governor::resolve_host_budget` both call this. #[derive(Clone, Debug, PartialEq, Eq)] pub enum HostBudgetJoinSource { CgroupMemoryHigh { cgroup_dir: String }, @@ -1629,14 +1626,15 @@ impl HostBudgetJoinSource { pub fn label(&self) -> String { match self { HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => { - format!("cgroup memory.high ({cgroup_dir})") + format!("cgroup memory.high ({})", cgroup_dir) } HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => { - format!("cgroup memory.max ({cgroup_dir})") - } - HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => { - format!("cgroup v1 memory.stat hierarchical_memory_limit ({cgroup_dir})") + format!("cgroup memory.max ({})", cgroup_dir) } + HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!( + "cgroup v1 memory.stat hierarchical_memory_limit ({})", + cgroup_dir + ), HostBudgetJoinSource::DarwinPhysicalMemory => "sysctl hw.memsize".to_string(), } } @@ -1671,7 +1669,6 @@ impl HostBudgetJoin { HostBudgetJoin::Unreadable { .. } => None, } } - pub fn label(&self) -> String { match self { HostBudgetJoin::Resolved { @@ -1681,17 +1678,21 @@ impl HostBudgetJoin { observed_bytes, } => match requested_bytes { Some(requested) => format!( - "effective planning minimum {effective_bytes} bytes (env request {requested}; observed {}={observed_bytes} bytes)", - source.label() + "effective planning minimum {} bytes (env request {}; observed {}={} bytes)", + effective_bytes, + requested, + source.label(), + observed_bytes ), None => source.label(), }, - HostBudgetJoin::Unreadable { reason } => format!("unreadable: {reason}"), + HostBudgetJoin::Unreadable { reason } => format!("unreadable: {}", reason), HostBudgetJoin::DeclaredUnverified { requested_bytes, reason, } => format!( - "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={requested_bytes}; {reason}" + "declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}", + requested_bytes, reason ), } } @@ -1706,13 +1707,7 @@ pub fn resolve_host_budget_join( ) -> HostBudgetJoin { let cgroup_v1_limit = match cgroup_v1_limit { Some((dir, HostBudgetCgroupV1::Unparseable(body))) => { - return HostBudgetJoin::Unreadable { - reason: format!( - "cgroup v1 memory hierarchy at {dir} holds this process but its \ - hierarchical_memory_limit is unreadable ({body}); a bound that may be the \ - tightest cannot be replaced by another reading" - ), - }; + return HostBudgetJoin::Unreadable { reason: format!("cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading", dir, body) }; } Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)), Some((_, HostBudgetCgroupV1::Unlimited)) | None => None, @@ -1758,26 +1753,11 @@ pub fn resolve_host_budget_join( }; } if let Some(requested_bytes) = env_override { - return HostBudgetJoin::DeclaredUnverified { - requested_bytes, - reason: "no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string(), - }; - } - HostBudgetJoin::Unreadable { - reason: format!( - "no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES \ - cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than \ - admitting against the widest signal available: a host-shared reading is a number \ - about the MACHINE, not about this slot, and admitting against one is the rc=137 \ - SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, \ - gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must \ - expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", - std::env::consts::OS - ), + return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: "no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit".to_string() }; } + HostBudgetJoin::Unreadable { reason: format!("no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.", std::env::consts::OS) } } -/// `(bytes, source label)` view of `resolve_host_budget_join` over the live observations. pub fn read_host_budget_bytes() -> (Option, String) { let join = resolve_host_budget_join( std::env::var("GUNBC_MEMORY_BUDGET_BYTES") @@ -1840,7 +1820,7 @@ pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> { let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?; let value = match std::fs::read_to_string(std::path::Path::new(&dir).join("memory.stat")) { Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size), - Err(e) => HostBudgetCgroupV1::Unparseable(format!("memory.stat: {e}")), + Err(e) => HostBudgetCgroupV1::Unparseable(format!("memory.stat: {}", e)), }; Some((dir, value)) } @@ -1906,8 +1886,6 @@ pub fn host_budget_darwin_physical() -> Option { let name = std::ffi::CStr::from_bytes_with_nul(b"hw.memsize\0").ok()?; let mut value: u64 = 0; let mut len: libc::size_t = std::mem::size_of::() as libc::size_t; - // SAFETY: `name` is a NUL-terminated literal, `value`/`len` are live locals sized to - // match, and `newp`/`newlen` are null/0 for a read-only query per sysctl(3). let rc = unsafe { libc::sysctlbyname( name.as_ptr(), From 0638cf52321774c2f107a49f3a2836227bbd3825 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 19:40:55 +0000 Subject: [PATCH 15/27] Close stage0 regen after merging main: emit the host-budget comment. One remote required-regen loop copied v1_rt.rs from the candidate; the second pass reported first_generation_equal=true. Co-authored-by: Cursor --- src/v1/stage0/src/v1_rt.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index 334a7bf9e36..ce655b46580 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1614,6 +1614,8 @@ pub fn contiguous_loop_elementwise_kernel( } out } +/// The one host-budget precedence. `read_host_budget_bytes` and +/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`. #[derive(Clone, Debug, PartialEq, Eq)] pub enum HostBudgetJoinSource { CgroupMemoryHigh { cgroup_dir: String }, From 5084eb42dc61b2ba43cc7d5ecbf9f0e7779166f0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 20:02:31 +0000 Subject: [PATCH 16/27] Identify stderr_capture by DeclarationRef, not last-segment name. A fixture-local WitnessStderrCapturePolicy must refuse as transport not modeled; only std.shell_stream_capture.WitnessStderrCapturePolicy admits the rust shell capture channels. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 2 + src/v1/05_emit.dag | 30 +++++++++---- src/v1/05_emit_rust.dag | 10 ++--- src/v1/stage0/src/cli_run/emit_host.rs | 10 +++++ src/v1/stage0/src/v1_compiler_emit.rs | 45 ++++++++++++++----- src/v1/stage0/src/v1_compiler_emit_rust.rs | 12 +++-- 6 files changed, 77 insertions(+), 32 deletions(-) diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag index 40c4018a719..9f9df6ab038 100644 --- a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag +++ b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag @@ -36,6 +36,7 @@ data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustificatio DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_STRING_POLICY", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_OPTIONAL_POLICY", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_COLLECTION_POLICY", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_HOMONYM_POLICY", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "GUNBC_MODULE_REACH_MEMBER", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "declared_stderr_capture_channels_do_not_refuse_the_union", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "an_unmodeled_shell_channel_still_refuses_the_union", field: WholeDeclaration }, @@ -44,6 +45,7 @@ data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustificatio DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_string_stderr_capture_refuse_the_union", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_optional_stderr_capture_refuse_the_union", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_collection_stderr_capture_refuse_the_union", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "capture_channels_with_homonym_stderr_capture_refuse_the_union", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "reaching_extdeps_gunbc_does_not_refuse_the_union_for_capture_channels", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "rustc_and_run_emitted_capture", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_bounded_tail_truncates_and_keeps_the_tail", field: WholeDeclaration }, diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index ec096cad3d3..a9e399702e8 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -37,10 +37,12 @@ import v1.std.core { Connective, Conj, Disj, NoConnective, Arrow, Required, CardOptional } -import v1.compiler.infer_env { TypeEnv, TypeBinding, authored_name, lookup_type_for, GlobalBareLookupState, empty_symbol_index } +import v1.compiler.infer_env { TypeEnv, TypeBinding, authored_name, lookup_type_for, GlobalBareLookupState, empty_symbol_index, type_reference_declaration_reading, TypeReferenceNamesDeclaration } import std.induction { InductiveField, SubValueUnknown } +import std.decl_ref { DeclarationRef, decl_ref, declaration_ref_eq } + import v1.compiler.infer_types { resolved_type, is_product_type, child_type_node, emit_map_has, node_is_collection, node_is_keyed_collection, node_is_element_collection, normalize_access_type_node, for_each_element_type_node, is_unit_like, is_declared_container_alias_spelling } import std.types { is_container_type } @@ -2589,17 +2591,29 @@ fn file_operation_has_content_input(op_node: Node, source_indices: Map) -> Bool { +fn required_stderr_capture_policy_declaration() -> DeclarationRef { + decl_ref(module_path: "std.shell_stream_capture", decl_name: "WitnessStderrCapturePolicy") +} + +fn param_type_is_stderr_capture_policy_declaration(p: Node, env: TypeEnv) -> Bool { + match type_reference_declaration_reading(n: param_node_type_expr(n: p), source_indices: env.source_indices, env: env) { + TypeReferenceNamesDeclaration { declaration: d } => + declaration_ref_eq(a: d, b: required_stderr_capture_policy_declaration()) + _ => false + } +} + +fn param_is_required_stderr_capture_policy(p: Node, env: TypeEnv) -> Bool { let ty = param_node_type_expr(n: p) - param_node_name_at(n: p, source_indices: source_indices) == "stderr_capture" + param_node_name_at(n: p, source_indices: env.source_indices) == "stderr_capture" && p.return_cardinality == Required && ty.return_cardinality == Required - && !node_is_collection(n: ty, source_indices: source_indices) - && qualified_last_segment(name: resolved_type_name(n: p, source_indices: source_indices)) == "WitnessStderrCapturePolicy" + && !node_is_collection(n: ty, source_indices: env.source_indices) + && param_type_is_stderr_capture_policy_declaration(p: p, env: env) } -fn operation_declares_required_stderr_capture_policy(op_node: Node, source_indices: Map) -> Bool { - op_node.params |> any(p => param_is_required_stderr_capture_policy(p: p, source_indices: source_indices)) +fn operation_declares_required_stderr_capture_policy(op_node: Node, env: TypeEnv) -> Bool { + op_node.params |> any(p => param_is_required_stderr_capture_policy(p: p, env: env)) } // The channels the interpreter's map_file_outputs answers, read back as the emitter's obligation. @@ -2843,7 +2857,7 @@ fn unmodeled_shell_transport_operation_diagnostics(tm: TypedModule, item: Node, Present { value: c } => if shell_channel_is_capture_accounting(c: c) && target_is_rust(target: target) - && !operation_declares_required_stderr_capture_policy(op_node: op_node, source_indices: si) + && !operation_declares_required_stderr_capture_policy(op_node: op_node, env: env) { [make_error_node( diagnostic: TransportEmissionNotModeled { diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index d0a813fb7fb..19e0086140d 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -16316,7 +16316,7 @@ fn emit_dry_run_branch_from_props(op_name: String, inferred: Node, mock_props: L fn emit_transport_call(bound: BoundOperation, op_name: String, registry: Map, depth: Int, service_item: Node, op_node: Node, source_indices: Map, shared_types: Set, env: TypeEnv) -> String { match bound { RestBound { transport: t } => emit_rest_call(op_name: op_name, transport: t, registry: registry, depth: depth, service_item: service_item, op_node: op_node, source_indices: source_indices, shared_types: shared_types, env: env) - ShellBound { transport: t, result_fields: rsf } => emit_shell_call(op_name: op_name, transport: t, registry: registry, depth: depth, result_fields: rsf, op_node: op_node, source_indices: source_indices) + ShellBound { transport: t, result_fields: rsf } => emit_shell_call(op_name: op_name, transport: t, registry: registry, depth: depth, result_fields: rsf, op_node: op_node, source_indices: source_indices, env: env) FileBound { verb: v, path_template: pt, result_fields: fs } => emit_file_call(verb: v, path_template: pt, result_fields: fs, source_indices: source_indices) LocalBound => emit_local_call(op_name: op_name) @@ -16878,7 +16878,7 @@ fn emit_exit_arm(prop: Node, result_fields: List, source_indic // Capture order: bind/validate the authored policy BEFORE spawn; start both drains // immediately; write stdin concurrently; join; then project BoundedTail or refuse // Complete overflow. -fn emit_shell_call(op_name: String, transport: Node, registry: Map, depth: Int, result_fields: List, op_node: Node, source_indices: Map) -> String { +fn emit_shell_call(op_name: String, transport: Node, registry: Map, depth: Int, result_fields: List, op_node: Node, source_indices: Map, env: TypeEnv) -> String { let argv = transport.children let optional_params = op_node.params |> filter(p => param_node_type_expr(n: p).return_cardinality == CardOptional) @@ -16919,7 +16919,7 @@ fn emit_shell_call(op_name: String, transport: Node, registry: Map) -> String { if shell_needs_capture_accounting(result_fields: result_fields) { "" } else { concat(shell_stderr_binding_line, " ") } } -fn emit_shell_stderr_policy_binding(op_node: Node, source_indices: Map) -> String { - if operation_declares_required_stderr_capture_policy(op_node: op_node, source_indices: source_indices) { +fn emit_shell_stderr_policy_binding(op_node: Node, env: TypeEnv) -> String { + if operation_declares_required_stderr_capture_policy(op_node: op_node, env: env) { concat( "let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n", " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n", diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 195d5704947..68f61c547ab 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3449,6 +3449,8 @@ mod fixture_closure_union_tests { const STDERR_CAPTURE_COLLECTION_POLICY: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: List\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const STDERR_CAPTURE_HOMONYM_POLICY: &str = "module efr_member\ntype WitnessStderrCapturePolicy { mark: String }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; + const GUNBC_MODULE_REACH_MEMBER: &str = "module efr_member\nimport extdeps.gunbc { packages }\nfn ignore() -> Int { 0 }\n"; @@ -3532,6 +3534,14 @@ mod fixture_closure_union_tests { ); } + #[test] + fn capture_channels_with_homonym_stderr_capture_refuse_the_union() { + capture_channels_refuse_unless_stderr_capture_is_the_required_scalar_policy( + STDERR_CAPTURE_HOMONYM_POLICY, + "homonym", + ); + } + fn rustc_and_run_emitted_capture( stem: &str, complete_limit: Option, diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index bd0dd3c6b55..7821383864f 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -5426,34 +5426,55 @@ pub fn file_operation_has_content_input( ) } -pub fn param_is_required_stderr_capture_policy( - p: Rc, - source_indices: Rc>>, -) -> bool { +pub fn required_stderr_capture_policy_declaration() -> Rc { + crate::std_decl_ref::decl_ref( + "std.shell_stream_capture".to_string(), + "WitnessStderrCapturePolicy".to_string(), + ) +} + +pub fn param_type_is_stderr_capture_policy_declaration(p: Rc, env: Rc) -> bool { + match (*crate::v1_compiler_infer_env::type_reference_declaration_reading( + crate::v1_std_core::param_node_type_expr(p.clone()), + env.source_indices.clone(), + env.clone(), + )) + .clone() + { + crate::v1_compiler_infer_env::TypeReferenceDeclarationReading::TypeReferenceNamesDeclaration { + declaration: d, + .. + } => crate::std_decl_ref::declaration_ref_eq( + d.clone(), + required_stderr_capture_policy_declaration(), + ), + _ => false, + } +} + +pub fn param_is_required_stderr_capture_policy(p: Rc, env: Rc) -> bool { { let ty = crate::v1_std_core::param_node_type_expr(p.clone()); - (((((crate::v1_std_core::param_node_name_at(p.clone(), source_indices.clone()) + (((((crate::v1_std_core::param_node_name_at(p.clone(), env.source_indices.clone()) == "stderr_capture".to_string()) && (p.return_cardinality.clone() == Cardinality::Required)) && (ty.return_cardinality.clone() == Cardinality::Required)) && !crate::v1_compiler_infer_types::node_is_collection( ty.clone(), - source_indices.clone(), + env.source_indices.clone(), )) - && (crate::v1_std_core::qualified_last_segment( - crate::v1_compiler_infer::resolved_type_name(p.clone(), source_indices.clone()), - ) == "WitnessStderrCapturePolicy".to_string())) + && param_type_is_stderr_capture_policy_declaration(p.clone(), env.clone())) } } pub fn operation_declares_required_stderr_capture_policy( op_node: Rc, - source_indices: Rc>>, + env: Rc, ) -> bool { { let mut __found = false; for p in op_node.params.clone().iter().cloned() { - if param_is_required_stderr_capture_policy(p.clone(), source_indices.clone()) { + if param_is_required_stderr_capture_policy(p.clone(), env.clone()) { __found = true; break; } @@ -5787,7 +5808,7 @@ match crate::v1_std_core::classify_transport(t.clone(), si.clone()) { span: ch.span.clone(), }), module_name.clone())]) }, - Some(c) => if ((shell_channel_is_capture_accounting(c.clone()) && target_is_rust(target.clone())) && !operation_declares_required_stderr_capture_policy(op_node.clone(), si.clone())) { + Some(c) => if ((shell_channel_is_capture_accounting(c.clone()) && target_is_rust(target.clone())) && !operation_declares_required_stderr_capture_policy(op_node.clone(), env.clone())) { Rc::new(vec![crate::v1_std_core::make_error_node(Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { transport_kind: "shell".to_string(), service: service_name.clone(), diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 571a14b2922..79bb8e99b36 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -37446,6 +37446,7 @@ pub fn emit_transport_call( rsf.clone(), op_node.clone(), source_indices.clone(), + env.clone(), ), BoundOperation::FileBound { verb: v, @@ -38883,6 +38884,7 @@ pub fn emit_shell_call( result_fields: Rc>>, op_node: Rc, source_indices: Rc>>, + env: Rc, ) -> String { { let argv = transport.children.clone(); @@ -39064,8 +39066,7 @@ pub fn emit_shell_call( ); if needs_capture.clone() { { - let policy_bind = - emit_shell_stderr_policy_binding(op_node.clone(), source_indices.clone()); + let policy_bind = emit_shell_stderr_policy_binding(op_node.clone(), env.clone()); let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); let spawn_exec = " .spawn()?;".to_string(); let stdin_thread = if has_stdin.clone() { @@ -39392,13 +39393,10 @@ pub fn shell_error_stderr_binding(result_fields: Rc>>) } } -pub fn emit_shell_stderr_policy_binding( - op_node: Rc, - source_indices: Rc>>, -) -> String { +pub fn emit_shell_stderr_policy_binding(op_node: Rc, env: Rc) -> String { if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( op_node.clone(), - source_indices.clone(), + env.clone(), ) { v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n".to_string(), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n".to_string()), " match v1_rt::read_host_budget_bytes() {\n".to_string()), " (Some(n), source) => (Some(n as usize), source),\n".to_string()), " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n".to_string()), " }\n".to_string()), " }\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n".to_string()), " let n = crate::std_measure::byte_size_count(bytes.clone());\n".to_string()), " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n".to_string()), " (None, String::new())\n".to_string()), " }\n".to_string()), "};\n".to_string()), "let __stderr_tail_bytes: usize = match &*stderr_capture {\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n".to_string()), "};\n".to_string()) } else { From 8584f64bbc9698344e75410acb9c259d306e81dc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 20:13:25 +0000 Subject: [PATCH 17/27] Delegate host-budget cgroup observation through v1_rt. Governor REDs now exercise the same walk and v1 parse Complete uses; the memory_governor copies are wrappers, matching Darwin physical. Co-authored-by: Cursor --- dag/gunbc/host/host_budget_source.dag | 4 +- src/v1/runtime_rust.dag | 29 ++++--- src/v1/stage0/src/memory_governor.rs | 109 ++++++-------------------- src/v1/stage0/src/v1_rt.rs | 46 ++++++++--- 4 files changed, 80 insertions(+), 108 deletions(-) diff --git a/dag/gunbc/host/host_budget_source.dag b/dag/gunbc/host/host_budget_source.dag index 2cb18c62a96..7898e4e7b69 100644 --- a/dag/gunbc/host/host_budget_source.dag +++ b/dag/gunbc/host/host_budget_source.dag @@ -333,8 +333,8 @@ fn kernel_budget_sourcing_static_source(g: KernelBudgetSourcing) -> HostBudgetSo // may bound a refusal; it may not stand in for a reading. // THE CGROUP-v1 ARM JOINS THIS SAME DECLARED MIRROR rather than opening a second one. The seed's -// memory_governor CgroupV1HierarchicalMemoryLimit arm, CgroupV1MemoryLimitValue, -// cgroup_v1_hierarchical_limit_from_stat and cgroup_v1_unlimited_bytes hand-realize +// v1_rt HostBudgetCgroupV1 arm, host_budget_cgroup_v1_from_stat and +// host_budget_cgroup_v1_unlimited_bytes hand-realize // BudgetSourceCgroupV1HierarchicalMemoryLimit here and extdeps.linux.cgroup_v1_memory's parse, under // the bootstrap constraint stated in the note below: the budget bounds the resolve, so it is read // before any .dag value exists. The mirror keeps the model's three states (limited / unlimited / diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index bcee5de0326..aae90ddb51d 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -1393,13 +1393,11 @@ fn rt_host_budget() -> String { " Unlimited,\n", " Unparseable(String),\n", "\}\n\n", - "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", - " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + "pub fn host_budget_tightest_cgroup_under(self_cg: &str, root: &std::path::Path, limit_file: &str) -> Option<(String, u64)> \{\n", " let rel = self_cg\n", " .lines()\n", " .find_map(|l| l.strip_prefix(\"0::\"))\n", " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n", - " let root = std::path::Path::new(\"/sys/fs/cgroup\");\n", " let mut dir = root.join(&rel);\n", " let mut best: Option<(u64, std::path::PathBuf)> = None;\n", " loop \{\n", @@ -1420,6 +1418,13 @@ fn rt_host_budget() -> String { " \}\n", " best.map(|(v, d)| (d.display().to_string(), v))\n", "\}\n\n", + "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> \{\n", + " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", + " host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new(\"/sys/fs/cgroup\"), limit_file)\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 \{\n", + " (i64::MAX as u64 / page_size) * page_size\n", + "\}\n\n", "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> \{\n", " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n", " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n", @@ -1430,15 +1435,19 @@ fn rt_host_budget() -> String { " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n", " ));\n", " \}\n", - " let page_size = page_size as u64;\n", - " let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n", - " let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) \{\n", + " host_budget_cgroup_v1_under(std::path::Path::new(\"/\"), &self_cg, &mountinfo, page_size as u64)\n", + "\}\n\n", + "pub fn host_budget_cgroup_v1_under(fs_root: &std::path::Path, self_cg: &str, mountinfo: &str, page_size: u64) -> Option<(String, HostBudgetCgroupV1)> \{\n", + " let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?;\n", + " let dir_path = std::path::Path::new(&dir);\n", + " let joined = fs_root.join(dir_path.strip_prefix(\"/\").unwrap_or(dir_path));\n", + " let value = match std::fs::read_to_string(joined.join(\"memory.stat\")) \{\n", " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n", " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: \{\}\", e)),\n", " \};\n", - " Some((dir, value))\n", + " Some((joined.display().to_string(), value))\n", "\}\n\n", - "fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option \{\n", + "pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option \{\n", " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| \{\n", " let fields: Vec<&str> = line.split(' ').collect();\n", " let dash = fields.iter().position(|f| *f == \"-\")?;\n", @@ -1464,10 +1473,10 @@ fn rt_host_budget() -> String { " \};\n", " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n", "\}\n\n", - "fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 \{\n", + "pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 \{\n", " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n", " let [body] = hits.as_slice() else \{ return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); \};\n", - " let unlimited = (i64::MAX as u64 / page_size) * page_size;\n", + " let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size);\n", " match body.trim().parse::() \{\n", " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n", " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n", diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 7524a3149ca..1de53b71802 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1375,29 +1375,8 @@ pub fn tightest_cgroup_dir_for(limit_file: &str) -> Option { /// `tightest_cgroup_dir_for` over supplied `/proc/self/cgroup` content and unified mount root, /// so a fixture hierarchy exercises the same walk. pub fn tightest_cgroup_dir_under(self_cg: &str, root: &Path, limit_file: &str) -> Option { - let rel = self_cg - .lines() - .find_map(|l| l.strip_prefix("0::")) - .map(|p| p.trim().trim_start_matches('/').to_string())?; - let mut dir = root.join(&rel); - let mut best: Option<(u64, PathBuf)> = None; - loop { - if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) { - let s = s.trim(); - if s != "max" { - if let Ok(v) = s.parse::() { - let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true); - if take { - best = Some((v, dir.clone())); - } - } - } - } - if dir == root || !dir.pop() { - break; - } - } - best.map(|(_, d)| d) + crate::v1_rt::host_budget_tightest_cgroup_under(self_cg, root, limit_file) + .map(|(dir, _)| PathBuf::from(dir)) } /// The process's own deepest (leaf) cgroup from `/proc/self/cgroup`. @@ -1441,41 +1420,13 @@ pub fn memory_pressure_some_avg10(content: &str) -> Option { /// mounted, the process has no line on it, or its path is outside the mounted subtree — each of /// which leaves the budget to the other sources or to `Unreadable`, never to a guess. pub fn cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { - let (mount_root, mount_point) = mountinfo.lines().find_map(|line| { - let fields: Vec<&str> = line.split(' ').collect(); - let dash = fields.iter().position(|f| *f == "-")?; - let fstype = fields.get(dash + 1)?; - let super_opts = fields.get(dash + 3)?; - if *fstype == "cgroup" && super_opts.split(',').any(|o| o == "memory") { - Some((fields.get(3)?.to_string(), fields.get(4)?.to_string())) - } else { - None - } - })?; - let path = self_cg.lines().find_map(|l| { - let mut parts = l.splitn(3, ':'); - let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?); - controllers - .split(',') - .any(|c| c == "memory") - .then(|| path.trim().to_string()) - })?; - let rel = if mount_root == "/" { - path.as_str() - } else { - let rest = path.strip_prefix(mount_root.as_str())?; - if !(rest.is_empty() || rest.starts_with('/')) { - return None; - } - rest - }; - Some(Path::new(&mount_point).join(rel.trim_start_matches('/'))) + crate::v1_rt::host_budget_cgroup_v1_memory_dir(self_cg, mountinfo).map(PathBuf::from) } /// Mirror of `extdeps.linux.cgroup_v1_memory` `cgroup_v1_unlimited_bytes`: the kernel's /// PAGE_COUNTER_MAX (LONG_MAX / PAGE_SIZE) reported in bytes, so the sentinel follows the page size. pub fn cgroup_v1_unlimited_bytes(page_size: u64) -> u64 { - (i64::MAX as u64 / page_size) * page_size + crate::v1_rt::host_budget_cgroup_v1_unlimited_bytes(page_size) } /// Mirror of `extdeps.linux.cgroup_v1_memory` `CgroupV1MemoryLimitValue`: three states, because @@ -1488,25 +1439,26 @@ pub enum CgroupV1MemoryLimitValue { } /// Mirror of `extdeps.linux.cgroup_v1_memory` `cgroup_v1_hierarchical_memory_limit`. +fn host_budget_cgroup_v1_value(v: crate::v1_rt::HostBudgetCgroupV1) -> CgroupV1MemoryLimitValue { + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + } +} + pub fn cgroup_v1_hierarchical_limit_from_stat( memory_stat: &str, page_size: u64, ) -> CgroupV1MemoryLimitValue { - let hits: Vec<&str> = memory_stat - .lines() - .filter_map(|l| l.trim().strip_prefix("hierarchical_memory_limit ")) - .collect(); - let [body] = hits.as_slice() else { - return CgroupV1MemoryLimitValue::Unparseable(memory_stat.to_string()); - }; - match body.trim().parse::() { - Ok(n) if n < 0 => CgroupV1MemoryLimitValue::Unparseable(body.to_string()), - Ok(n) if n >= cgroup_v1_unlimited_bytes(page_size) as i128 => { - CgroupV1MemoryLimitValue::Unlimited - } - Ok(n) => CgroupV1MemoryLimitValue::Limited(n as u64), - Err(_) => CgroupV1MemoryLimitValue::Unparseable(body.to_string()), - } + host_budget_cgroup_v1_value(crate::v1_rt::host_budget_cgroup_v1_from_stat( + memory_stat, + page_size, + )) } /// The v1 reading over supplied procfs content and a filesystem root (`/` in production, a @@ -1519,27 +1471,12 @@ pub fn cgroup_v1_hierarchical_limit_under( mountinfo: &str, page_size: u64, ) -> Option<(String, CgroupV1MemoryLimitValue)> { - let dir = cgroup_v1_memory_dir(self_cg, mountinfo)?; - let dir = fs_root.join(dir.strip_prefix("/").unwrap_or(&dir)); - let value = match std::fs::read_to_string(dir.join("memory.stat")) { - Ok(stat) => cgroup_v1_hierarchical_limit_from_stat(&stat, page_size), - Err(e) => CgroupV1MemoryLimitValue::Unparseable(format!("memory.stat: {e}")), - }; - Some((dir.display().to_string(), value)) + crate::v1_rt::host_budget_cgroup_v1_under(fs_root, self_cg, mountinfo, page_size) + .map(|(dir, v)| (dir, host_budget_cgroup_v1_value(v))) } pub fn read_cgroup_v1_hierarchical_limit() -> Option<(String, CgroupV1MemoryLimitValue)> { - let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; - let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; - // SAFETY: sysconf has no preconditions. - let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; - if page_size <= 0 { - return Some(( - "/proc/self/mountinfo".to_string(), - CgroupV1MemoryLimitValue::Unparseable("sysconf(_SC_PAGESIZE) unreadable".to_string()), - )); - } - cgroup_v1_hierarchical_limit_under(Path::new("/"), &self_cg, &mountinfo, page_size as u64) + crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| (dir, host_budget_cgroup_v1_value(v))) } pub fn read_cgroup_u64(dir: &Path, file: &str) -> Option { diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index ce655b46580..9d934b2f5f7 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -1780,13 +1780,15 @@ pub enum HostBudgetCgroupV1 { Unparseable(String), } -pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { - let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; +pub fn host_budget_tightest_cgroup_under( + self_cg: &str, + root: &std::path::Path, + limit_file: &str, +) -> Option<(String, u64)> { let rel = self_cg .lines() .find_map(|l| l.strip_prefix("0::")) .map(|p| p.trim().trim_start_matches('/').to_string())?; - let root = std::path::Path::new("/sys/fs/cgroup"); let mut dir = root.join(&rel); let mut best: Option<(u64, std::path::PathBuf)> = None; loop { @@ -1808,6 +1810,15 @@ pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { best.map(|(v, d)| (d.display().to_string(), v)) } +pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> { + let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; + host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new("/sys/fs/cgroup"), limit_file) +} + +pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 { + (i64::MAX as u64 / page_size) * page_size +} + pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> { let self_cg = std::fs::read_to_string("/proc/self/cgroup").ok()?; let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?; @@ -1818,16 +1829,31 @@ pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> { HostBudgetCgroupV1::Unparseable("sysconf(_SC_PAGESIZE) unreadable".to_string()), )); } - let page_size = page_size as u64; - let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?; - let value = match std::fs::read_to_string(std::path::Path::new(&dir).join("memory.stat")) { + host_budget_cgroup_v1_under( + std::path::Path::new("/"), + &self_cg, + &mountinfo, + page_size as u64, + ) +} + +pub fn host_budget_cgroup_v1_under( + fs_root: &std::path::Path, + self_cg: &str, + mountinfo: &str, + page_size: u64, +) -> Option<(String, HostBudgetCgroupV1)> { + let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?; + let dir_path = std::path::Path::new(&dir); + let joined = fs_root.join(dir_path.strip_prefix("/").unwrap_or(dir_path)); + let value = match std::fs::read_to_string(joined.join("memory.stat")) { Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size), Err(e) => HostBudgetCgroupV1::Unparseable(format!("memory.stat: {}", e)), }; - Some((dir, value)) + Some((joined.display().to_string(), value)) } -fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { +pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option { let (mount_root, mount_point) = mountinfo.lines().find_map(|line| { let fields: Vec<&str> = line.split(' ').collect(); let dash = fields.iter().position(|f| *f == "-")?; @@ -1864,7 +1890,7 @@ fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option HostBudgetCgroupV1 { +pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 { let hits: std::vec::Vec<&str> = memory_stat .lines() .filter_map(|l| l.trim().strip_prefix("hierarchical_memory_limit ")) @@ -1872,7 +1898,7 @@ fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBud let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); }; - let unlimited = (i64::MAX as u64 / page_size) * page_size; + let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size); match body.trim().parse::() { Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()), Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited, From c764d4763e2697b5c3d5e5e03abed7bed97a8a5d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 21:44:38 +0000 Subject: [PATCH 18/27] Drop the host-budget cgroup value adapter. Map HostBudgetCgroupV1 at the existing wrapper call sites so the consolidation adds no new hand-Rust item. Co-authored-by: Cursor --- src/v1/stage0/src/memory_governor.rs | 52 ++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/src/v1/stage0/src/memory_governor.rs b/src/v1/stage0/src/memory_governor.rs index 1de53b71802..bc9eadf2827 100644 --- a/src/v1/stage0/src/memory_governor.rs +++ b/src/v1/stage0/src/memory_governor.rs @@ -1439,8 +1439,11 @@ pub enum CgroupV1MemoryLimitValue { } /// Mirror of `extdeps.linux.cgroup_v1_memory` `cgroup_v1_hierarchical_memory_limit`. -fn host_budget_cgroup_v1_value(v: crate::v1_rt::HostBudgetCgroupV1) -> CgroupV1MemoryLimitValue { - match v { +pub fn cgroup_v1_hierarchical_limit_from_stat( + memory_stat: &str, + page_size: u64, +) -> CgroupV1MemoryLimitValue { + match crate::v1_rt::host_budget_cgroup_v1_from_stat(memory_stat, page_size) { crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { CgroupV1MemoryLimitValue::Limited(bytes) } @@ -1451,16 +1454,6 @@ fn host_budget_cgroup_v1_value(v: crate::v1_rt::HostBudgetCgroupV1) -> CgroupV1M } } -pub fn cgroup_v1_hierarchical_limit_from_stat( - memory_stat: &str, - page_size: u64, -) -> CgroupV1MemoryLimitValue { - host_budget_cgroup_v1_value(crate::v1_rt::host_budget_cgroup_v1_from_stat( - memory_stat, - page_size, - )) -} - /// The v1 reading over supplied procfs content and a filesystem root (`/` in production, a /// fixture directory in tests), so the real route — locate, read, parse — runs under test. /// `None` only when no v1 memory hierarchy holds this process; once one does, an unreadable @@ -1471,12 +1464,41 @@ pub fn cgroup_v1_hierarchical_limit_under( mountinfo: &str, page_size: u64, ) -> Option<(String, CgroupV1MemoryLimitValue)> { - crate::v1_rt::host_budget_cgroup_v1_under(fs_root, self_cg, mountinfo, page_size) - .map(|(dir, v)| (dir, host_budget_cgroup_v1_value(v))) + crate::v1_rt::host_budget_cgroup_v1_under(fs_root, self_cg, mountinfo, page_size).map( + |(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => { + CgroupV1MemoryLimitValue::Unlimited + } + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) + }, + ) } pub fn read_cgroup_v1_hierarchical_limit() -> Option<(String, CgroupV1MemoryLimitValue)> { - crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| (dir, host_budget_cgroup_v1_value(v))) + crate::v1_rt::host_budget_cgroup_v1().map(|(dir, v)| { + ( + dir, + match v { + crate::v1_rt::HostBudgetCgroupV1::Limited(bytes) => { + CgroupV1MemoryLimitValue::Limited(bytes) + } + crate::v1_rt::HostBudgetCgroupV1::Unlimited => CgroupV1MemoryLimitValue::Unlimited, + crate::v1_rt::HostBudgetCgroupV1::Unparseable(body) => { + CgroupV1MemoryLimitValue::Unparseable(body) + } + }, + ) + }) } pub fn read_cgroup_u64(dir: &Path, file: &str) -> Option { From ea3ede104b4cda3497391951770d9a793a0c2171 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 6 Oct 2026 22:14:17 +0000 Subject: [PATCH 19/27] Install stage0 mirrors for stderr-capture DeclarationRef and host-budget observers. required-regen at c764 drifted v1_compiler_emit.rs and rust_runtime_source; v1_rt stays the live observers until the next generation emits from this source. Co-authored-by: Cursor --- src/v1/stage0/src/v1_compiler_emit.rs | 18 ++++++++++++------ src/v1/stage0/src/v1_compiler_runtime_rust.rs | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 7821383864f..99ba0f8df7b 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -21,6 +21,8 @@ pub use crate::std_coercion::TypeDeclarationProvenance; use crate::std_coercion::TypeDeclarationProvenance::DeclarationIdentityAbsent; pub use crate::std_coercion::TypeRealizationDecision; use crate::std_coercion::TypeRealizationDecision::*; +pub use crate::std_decl_ref::DeclarationRef; +pub use crate::std_decl_ref::{decl_ref, declaration_ref_eq}; use crate::std_induction::SubValueRelation::SubValueUnknown; pub use crate::std_induction::{InductiveField, SubValueRelation}; pub use crate::std_occurrence_identity::occurrence_id_eq; @@ -63,9 +65,14 @@ use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::{ }; pub use crate::v1_compiler_infer_emit_info::{DataVariantWireSpelling, EmitGraphInfo, TypeSummary}; use crate::v1_compiler_infer_env::GlobalBareLookupState::*; +use crate::v1_compiler_infer_env::TypeReferenceDeclarationReading::TypeReferenceNamesDeclaration; pub use crate::v1_compiler_infer_env::UnitVariantContribution; -pub use crate::v1_compiler_infer_env::{authored_name, empty_symbol_index, lookup_type_for}; -pub use crate::v1_compiler_infer_env::{GlobalBareLookupState, TypeBinding, TypeEnv}; +pub use crate::v1_compiler_infer_env::{ + authored_name, empty_symbol_index, lookup_type_for, type_reference_declaration_reading, +}; +pub use crate::v1_compiler_infer_env::{ + GlobalBareLookupState, TypeBinding, TypeEnv, TypeReferenceDeclarationReading, +}; pub use crate::v1_compiler_infer_items::{item_is_effectful_callee, item_resource_names}; pub use crate::v1_compiler_infer_items::{ItemInfo, ResolvedGraph, TypedModule}; pub use crate::v1_compiler_infer_lookup::lookup_func_sig; @@ -5426,7 +5433,7 @@ pub fn file_operation_has_content_input( ) } -pub fn required_stderr_capture_policy_declaration() -> Rc { +pub fn required_stderr_capture_policy_declaration() -> Rc { crate::std_decl_ref::decl_ref( "std.shell_stream_capture".to_string(), "WitnessStderrCapturePolicy".to_string(), @@ -5441,9 +5448,8 @@ pub fn param_type_is_stderr_capture_policy_declaration(p: Rc, env: Rc crate::std_decl_ref::declaration_ref_eq( d.clone(), required_stderr_capture_policy_declaration(), diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 4c09296fbe6..dde63571dd1 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -173,7 +173,7 @@ pub fn rust_runtime_source() -> String { } pub fn rt_host_budget() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The one host-budget precedence. `read_host_budget_bytes` and\n".to_string(), "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoinSource {\n".to_string()), " CgroupMemoryHigh { cgroup_dir: String },\n".to_string()), " CgroupMemoryMax { cgroup_dir: String },\n".to_string()), " CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n".to_string()), " DarwinPhysicalMemory,\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoinSource {\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoin {\n".to_string()), " Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n".to_string()), " DeclaredUnverified { requested_bytes: u64, reason: String },\n".to_string()), " Unreadable { reason: String },\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoin {\n".to_string()), " pub fn bytes(&self) -> Option {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n".to_string()), " HostBudgetJoin::Unreadable { .. } => None,\n".to_string()), " }\n".to_string()), " }\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n".to_string()), " Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n".to_string()), " None => source.label(),\n".to_string()), " },\n".to_string()), " HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n".to_string()), " let cgroup_v1_limit = match cgroup_v1_limit {\n".to_string()), " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n".to_string()), " return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n".to_string()), " }\n".to_string()), " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n".to_string()), " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n".to_string()), " };\n".to_string()), " let observation = [\n".to_string()), " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n".to_string()), " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n".to_string()), " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n".to_string()), " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n".to_string()), " if let Some((source, observed_bytes)) = observation {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n".to_string()), " }\n".to_string()), " if let Some(bytes) = darwin_physical {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n".to_string()), " }\n".to_string()), " if let Some(requested_bytes) = env_override {\n".to_string()), " return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n".to_string()), " }\n".to_string()), " HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n".to_string()), "}\n\n".to_string()), "pub fn read_host_budget_bytes() -> (Option, String) {\n".to_string()), " let join = resolve_host_budget_join(\n".to_string()), " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n".to_string()), " host_budget_tightest_cgroup(\"memory.high\"),\n".to_string()), " host_budget_tightest_cgroup(\"memory.max\"),\n".to_string()), " host_budget_cgroup_v1(),\n".to_string()), " host_budget_darwin_physical(),\n".to_string()), " );\n".to_string()), " (join.bytes(), join.label())\n".to_string()), "}\n\n".to_string()), "#[derive(Clone)]\n".to_string()), "pub enum HostBudgetCgroupV1 {\n".to_string()), " Limited(u64),\n".to_string()), " Unlimited,\n".to_string()), " Unparseable(String),\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let rel = self_cg\n".to_string()), " .lines()\n".to_string()), " .find_map(|l| l.strip_prefix(\"0::\"))\n".to_string()), " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n".to_string()), " let root = std::path::Path::new(\"/sys/fs/cgroup\");\n".to_string()), " let mut dir = root.join(&rel);\n".to_string()), " let mut best: Option<(u64, std::path::PathBuf)> = None;\n".to_string()), " loop {\n".to_string()), " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n".to_string()), " let s = s.trim();\n".to_string()), " if s != \"max\" {\n".to_string()), " if let Ok(v) = s.parse::() {\n".to_string()), " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n".to_string()), " if take {\n".to_string()), " best = Some((v, dir.clone()));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " if dir == root || !dir.pop() {\n".to_string()), " break;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " best.map(|(v, d)| (d.display().to_string(), v))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n".to_string()), " let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n".to_string()), " if page_size <= 0 {\n".to_string()), " return Some((\n".to_string()), " \"/proc/self/mountinfo\".to_string(),\n".to_string()), " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n".to_string()), " ));\n".to_string()), " }\n".to_string()), " let page_size = page_size as u64;\n".to_string()), " let dir = host_budget_cgroup_v1_memory_dir(&self_cg, &mountinfo)?;\n".to_string()), " let value = match std::fs::read_to_string(std::path::Path::new(&dir).join(\"memory.stat\")) {\n".to_string()), " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n".to_string()), " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n".to_string()), " };\n".to_string()), " Some((dir, value))\n".to_string()), "}\n\n".to_string()), "fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n".to_string()), " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n".to_string()), " let fields: Vec<&str> = line.split(' ').collect();\n".to_string()), " let dash = fields.iter().position(|f| *f == \"-\")?;\n".to_string()), " let fstype = fields.get(dash + 1)?;\n".to_string()), " let super_opts = fields.get(dash + 3)?;\n".to_string()), " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n".to_string()), " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n".to_string()), " } else {\n".to_string()), " None\n".to_string()), " }\n".to_string()), " })?;\n".to_string()), " let path = self_cg.lines().find_map(|l| {\n".to_string()), " let mut parts = l.splitn(3, ':');\n".to_string()), " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n".to_string()), " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n".to_string()), " })?;\n".to_string()), " let rel = if mount_root == \"/\" {\n".to_string()), " path.as_str()\n".to_string()), " } else {\n".to_string()), " let rest = path.strip_prefix(mount_root.as_str())?;\n".to_string()), " if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n".to_string()), " rest\n".to_string()), " };\n".to_string()), " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n".to_string()), "}\n\n".to_string()), "fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n".to_string()), " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n".to_string()), " let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n".to_string()), " let unlimited = (i64::MAX as u64 / page_size) * page_size;\n".to_string()), " match body.trim().parse::() {\n".to_string()), " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n".to_string()), " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n".to_string()), " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n".to_string()), "pub fn host_budget_darwin_physical() -> Option {\n".to_string()), " #[cfg(target_os = \"macos\")]\n".to_string()), " {\n".to_string()), " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n".to_string()), " let mut value: u64 = 0;\n".to_string()), " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n".to_string()), " let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n".to_string()), " if rc == 0 && value > 0 { Some(value) } else { None }\n".to_string()), " }\n".to_string()), " #[cfg(not(target_os = \"macos\"))]\n".to_string()), " { None }\n".to_string()), "}\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The one host-budget precedence. `read_host_budget_bytes` and\n".to_string(), "/// `memory_governor::resolve_host_budget` both call `resolve_host_budget_join`.\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoinSource {\n".to_string()), " CgroupMemoryHigh { cgroup_dir: String },\n".to_string()), " CgroupMemoryMax { cgroup_dir: String },\n".to_string()), " CgroupV1HierarchicalMemoryLimit { cgroup_dir: String },\n".to_string()), " DarwinPhysicalMemory,\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoinSource {\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir } => format!(\"cgroup memory.high ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir } => format!(\"cgroup memory.max ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir } => format!(\"cgroup v1 memory.stat hierarchical_memory_limit ({})\", cgroup_dir),\n".to_string()), " HostBudgetJoinSource::DarwinPhysicalMemory => \"sysctl hw.memsize\".to_string(),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "#[derive(Clone, Debug, PartialEq, Eq)]\n".to_string()), "pub enum HostBudgetJoin {\n".to_string()), " Resolved { effective_bytes: u64, requested_bytes: Option, source: HostBudgetJoinSource, observed_bytes: u64 },\n".to_string()), " DeclaredUnverified { requested_bytes: u64, reason: String },\n".to_string()), " Unreadable { reason: String },\n".to_string()), "}\n\n".to_string()), "impl HostBudgetJoin {\n".to_string()), " pub fn bytes(&self) -> Option {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, .. } => Some(*effective_bytes),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, .. } => Some(*requested_bytes),\n".to_string()), " HostBudgetJoin::Unreadable { .. } => None,\n".to_string()), " }\n".to_string()), " }\n".to_string()), " pub fn label(&self) -> String {\n".to_string()), " match self {\n".to_string()), " HostBudgetJoin::Resolved { effective_bytes, requested_bytes, source, observed_bytes } => match requested_bytes {\n".to_string()), " Some(requested) => format!(\"effective planning minimum {} bytes (env request {}; observed {}={} bytes)\", effective_bytes, requested, source.label(), observed_bytes),\n".to_string()), " None => source.label(),\n".to_string()), " },\n".to_string()), " HostBudgetJoin::Unreadable { reason } => format!(\"unreadable: {}\", reason),\n".to_string()), " HostBudgetJoin::DeclaredUnverified { requested_bytes, reason } => format!(\"declared-unverified: env GUNBC_MEMORY_BUDGET_BYTES={}; {}\", requested_bytes, reason),\n".to_string()), " }\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "pub fn resolve_host_budget_join(env_override: Option, cgroup_high: Option<(String, u64)>, cgroup_max: Option<(String, u64)>, cgroup_v1_limit: Option<(String, HostBudgetCgroupV1)>, darwin_physical: Option) -> HostBudgetJoin {\n".to_string()), " let cgroup_v1_limit = match cgroup_v1_limit {\n".to_string()), " Some((dir, HostBudgetCgroupV1::Unparseable(body))) => {\n".to_string()), " return HostBudgetJoin::Unreadable { reason: format!(\"cgroup v1 memory hierarchy at {} holds this process but its hierarchical_memory_limit is unreadable ({}); a bound that may be the tightest cannot be replaced by another reading\", dir, body) };\n".to_string()), " }\n".to_string()), " Some((dir, HostBudgetCgroupV1::Limited(bytes))) => Some((dir, bytes)),\n".to_string()), " Some((_, HostBudgetCgroupV1::Unlimited)) | None => None,\n".to_string()), " };\n".to_string()), " let observation = [\n".to_string()), " cgroup_high.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryHigh { cgroup_dir }, b)),\n".to_string()), " cgroup_max.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupMemoryMax { cgroup_dir }, b)),\n".to_string()), " cgroup_v1_limit.map(|(cgroup_dir, b)| (HostBudgetJoinSource::CgroupV1HierarchicalMemoryLimit { cgroup_dir }, b)),\n".to_string()), " ].into_iter().flatten().fold(None::<(HostBudgetJoinSource, u64)>, |best, cand| match best { Some(cur) if cur.1 <= cand.1 => Some(cur), _ => Some(cand) });\n".to_string()), " if let Some((source, observed_bytes)) = observation {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(observed_bytes)).unwrap_or(observed_bytes), requested_bytes: env_override, source, observed_bytes };\n".to_string()), " }\n".to_string()), " if let Some(bytes) = darwin_physical {\n".to_string()), " return HostBudgetJoin::Resolved { effective_bytes: env_override.map(|requested| requested.min(bytes)).unwrap_or(bytes), requested_bytes: env_override, source: HostBudgetJoinSource::DarwinPhysicalMemory, observed_bytes: bytes };\n".to_string()), " }\n".to_string()), " if let Some(requested_bytes) = env_override {\n".to_string()), " return HostBudgetJoin::DeclaredUnverified { requested_bytes, reason: \"no observed private memory.high, memory.max or v1 hierarchical_memory_limit verifies the executor allowance; the declaration is a planning request, not an enforced process limit\".to_string() };\n".to_string()), " }\n".to_string()), " HostBudgetJoin::Unreadable { reason: format!(\"no cgroup memory.high, memory.max or v1 hierarchical_memory_limit binds this process and GUNBC_MEMORY_BUDGET_BYTES cannot verify one (target_os={}), so the planning allowance is UNKNOWN. Refusing rather than admitting against the widest signal available: a host-shared reading is a number about the MACHINE, not about this slot, and admitting against one is the rc=137 SIGKILL this arm exists to prevent (BuildBuddy receipt 2026-08-30, gunbc.host_budget_source host_budget_source_seed_mirror_disposition). The executor must expose an enforceable limit; GUNBC_MEMORY_BUDGET_BYTES may only request a lower planning ceiling.\", std::env::consts::OS) }\n".to_string()), "}\n\n".to_string()), "pub fn read_host_budget_bytes() -> (Option, String) {\n".to_string()), " let join = resolve_host_budget_join(\n".to_string()), " std::env::var(\"GUNBC_MEMORY_BUDGET_BYTES\").ok().and_then(|s| s.trim().parse::().ok()),\n".to_string()), " host_budget_tightest_cgroup(\"memory.high\"),\n".to_string()), " host_budget_tightest_cgroup(\"memory.max\"),\n".to_string()), " host_budget_cgroup_v1(),\n".to_string()), " host_budget_darwin_physical(),\n".to_string()), " );\n".to_string()), " (join.bytes(), join.label())\n".to_string()), "}\n\n".to_string()), "#[derive(Clone)]\n".to_string()), "pub enum HostBudgetCgroupV1 {\n".to_string()), " Limited(u64),\n".to_string()), " Unlimited,\n".to_string()), " Unparseable(String),\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup_under(self_cg: &str, root: &std::path::Path, limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let rel = self_cg\n".to_string()), " .lines()\n".to_string()), " .find_map(|l| l.strip_prefix(\"0::\"))\n".to_string()), " .map(|p| p.trim().trim_start_matches('/').to_string())?;\n".to_string()), " let mut dir = root.join(&rel);\n".to_string()), " let mut best: Option<(u64, std::path::PathBuf)> = None;\n".to_string()), " loop {\n".to_string()), " if let Ok(s) = std::fs::read_to_string(dir.join(limit_file)) {\n".to_string()), " let s = s.trim();\n".to_string()), " if s != \"max\" {\n".to_string()), " if let Ok(v) = s.parse::() {\n".to_string()), " let take = best.as_ref().map(|(cur, _)| v < *cur).unwrap_or(true);\n".to_string()), " if take {\n".to_string()), " best = Some((v, dir.clone()));\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " }\n".to_string()), " if dir == root || !dir.pop() {\n".to_string()), " break;\n".to_string()), " }\n".to_string()), " }\n".to_string()), " best.map(|(v, d)| (d.display().to_string(), v))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_tightest_cgroup(limit_file: &str) -> Option<(String, u64)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " host_budget_tightest_cgroup_under(&self_cg, std::path::Path::new(\"/sys/fs/cgroup\"), limit_file)\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_unlimited_bytes(page_size: u64) -> u64 {\n".to_string()), " (i64::MAX as u64 / page_size) * page_size\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1() -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let self_cg = std::fs::read_to_string(\"/proc/self/cgroup\").ok()?;\n".to_string()), " let mountinfo = std::fs::read_to_string(\"/proc/self/mountinfo\").ok()?;\n".to_string()), " let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };\n".to_string()), " if page_size <= 0 {\n".to_string()), " return Some((\n".to_string()), " \"/proc/self/mountinfo\".to_string(),\n".to_string()), " HostBudgetCgroupV1::Unparseable(\"sysconf(_SC_PAGESIZE) unreadable\".to_string()),\n".to_string()), " ));\n".to_string()), " }\n".to_string()), " host_budget_cgroup_v1_under(std::path::Path::new(\"/\"), &self_cg, &mountinfo, page_size as u64)\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_under(fs_root: &std::path::Path, self_cg: &str, mountinfo: &str, page_size: u64) -> Option<(String, HostBudgetCgroupV1)> {\n".to_string()), " let dir = host_budget_cgroup_v1_memory_dir(self_cg, mountinfo)?;\n".to_string()), " let dir_path = std::path::Path::new(&dir);\n".to_string()), " let joined = fs_root.join(dir_path.strip_prefix(\"/\").unwrap_or(dir_path));\n".to_string()), " let value = match std::fs::read_to_string(joined.join(\"memory.stat\")) {\n".to_string()), " Ok(stat) => host_budget_cgroup_v1_from_stat(&stat, page_size),\n".to_string()), " Err(e) => HostBudgetCgroupV1::Unparseable(format!(\"memory.stat: {}\", e)),\n".to_string()), " };\n".to_string()), " Some((joined.display().to_string(), value))\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_memory_dir(self_cg: &str, mountinfo: &str) -> Option {\n".to_string()), " let (mount_root, mount_point) = mountinfo.lines().find_map(|line| {\n".to_string()), " let fields: Vec<&str> = line.split(' ').collect();\n".to_string()), " let dash = fields.iter().position(|f| *f == \"-\")?;\n".to_string()), " let fstype = fields.get(dash + 1)?;\n".to_string()), " let super_opts = fields.get(dash + 3)?;\n".to_string()), " if *fstype == \"cgroup\" && super_opts.split(',').any(|o| o == \"memory\") {\n".to_string()), " Some((fields.get(3)?.to_string(), fields.get(4)?.to_string()))\n".to_string()), " } else {\n".to_string()), " None\n".to_string()), " }\n".to_string()), " })?;\n".to_string()), " let path = self_cg.lines().find_map(|l| {\n".to_string()), " let mut parts = l.splitn(3, ':');\n".to_string()), " let (_id, controllers, path) = (parts.next()?, parts.next()?, parts.next()?);\n".to_string()), " controllers.split(',').any(|c| c == \"memory\").then(|| path.trim().to_string())\n".to_string()), " })?;\n".to_string()), " let rel = if mount_root == \"/\" {\n".to_string()), " path.as_str()\n".to_string()), " } else {\n".to_string()), " let rest = path.strip_prefix(mount_root.as_str())?;\n".to_string()), " if !(rest.is_empty() || rest.starts_with('/')) { return None; }\n".to_string()), " rest\n".to_string()), " };\n".to_string()), " Some(std::path::Path::new(&mount_point).join(rel.trim_start_matches('/')).display().to_string())\n".to_string()), "}\n\n".to_string()), "pub fn host_budget_cgroup_v1_from_stat(memory_stat: &str, page_size: u64) -> HostBudgetCgroupV1 {\n".to_string()), " let hits: std::vec::Vec<&str> = memory_stat.lines().filter_map(|l| l.trim().strip_prefix(\"hierarchical_memory_limit \")).collect();\n".to_string()), " let [body] = hits.as_slice() else { return HostBudgetCgroupV1::Unparseable(memory_stat.to_string()); };\n".to_string()), " let unlimited = host_budget_cgroup_v1_unlimited_bytes(page_size);\n".to_string()), " match body.trim().parse::() {\n".to_string()), " Ok(n) if n < 0 => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " Ok(n) if n >= unlimited as i128 => HostBudgetCgroupV1::Unlimited,\n".to_string()), " Ok(n) => HostBudgetCgroupV1::Limited(n as u64),\n".to_string()), " Err(_) => HostBudgetCgroupV1::Unparseable(body.to_string()),\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "/// Darwin `hw.memsize` via `sysctlbyname`. Authority: `extdeps.darwin.sysctl` `HwMemsize`.\n".to_string()), "pub fn host_budget_darwin_physical() -> Option {\n".to_string()), " #[cfg(target_os = \"macos\")]\n".to_string()), " {\n".to_string()), " let name = std::ffi::CStr::from_bytes_with_nul(b\"hw.memsize\\0\").ok()?;\n".to_string()), " let mut value: u64 = 0;\n".to_string()), " let mut len: libc::size_t = std::mem::size_of::() as libc::size_t;\n".to_string()), " let rc = unsafe { libc::sysctlbyname(name.as_ptr(), (&mut value as *mut u64).cast::(), &mut len, std::ptr::null_mut(), 0) };\n".to_string()), " if rc == 0 && value > 0 { Some(value) } else { None }\n".to_string()), " }\n".to_string()), " #[cfg(not(target_os = \"macos\"))]\n".to_string()), " { None }\n".to_string()), "}\n".to_string()) } pub fn rt_accelerator_demo_kernel() -> String { From ab5dded01520faeec55aabc54bbfdfe926003c77 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 18:49:27 +0000 Subject: [PATCH 20/27] Install v1_rt.rs from required-regen after the main merge. The merge-head seed emits list concat via append; the pre-merge mirror still used extend. Co-authored-by: Cursor --- src/v1/stage0/src/v1_rt.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index 9d934b2f5f7..fffaf7cad8c 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -265,7 +265,7 @@ impl V2Concat for String { impl V2Concat for Vec { fn v1_concat(mut self, other: Vec) -> Vec { - self.extend(other); + self.append(other); self } } @@ -749,7 +749,7 @@ pub fn map_values(m: &HashMap) -> Vec { } pub fn list_concat(mut a: Vec, b: Vec) -> Vec { - a.extend(b); + a.append(b); a } @@ -850,7 +850,7 @@ pub fn rc_list_push(list: Rc>, item: T) -> Rc> { pub fn rc_list_concat(a: Rc>, b: Rc>) -> Rc> { let mut result = a; - Rc::make_mut(&mut result).extend(b.iter().cloned()); + Rc::make_mut(&mut result).append((*b).clone()); result } From c87fad00ba7d0f6f867706f479818e5dd5fed2d1 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 19:26:47 +0000 Subject: [PATCH 21/27] Restore #[test] placement after the main merge splice of emit_host. The capture-gap attribute landed on STDERR_CAPTURE_MEMBER and clippy refused the lint step. Co-authored-by: Cursor --- src/v1/stage0/src/cli_run/emit_host.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 8403860bb1b..4d218fe11cb 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3879,7 +3879,6 @@ mod fixture_closure_union_tests { assert!(union.conflicts.contains("dag/a.dag")); } - #[test] const STDERR_CAPTURE_MEMBER: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; const STDERR_CAPTURE_UNMODELED_SIBLING: &str = "module efr_member\nimport std.shell_stream_capture { WitnessStderrCapturePolicy, BoundedTail }\nimport std.measure { byte_size }\nservice Bin {\n operation Run {\n input {\n bin_path: String\n stderr_capture: WitnessStderrCapturePolicy = BoundedTail { bytes: byte_size(count: 16) }\n }\n output {\n success: Bool from \"exit_success\"\n stderr_truncated: Bool from \"stderr_truncated\"\n stderr_total_bytes: Int from \"stderr_total_bytes\"\n stderr_retained_bytes: Int from \"stderr_retained_bytes\"\n }\n transport shell { argv: [\"{bin_path}\"] }\n }\n operation Weird {\n input { bin_path: String }\n output { digest: String from \"stderr_digest_hex\" }\n transport shell { argv: [\"{bin_path}\"] }\n }\n}\n"; @@ -4236,6 +4235,8 @@ mod fixture_closure_union_tests { panic!("a compile-probe reach of extdeps.gunbc must emit: {refusal}"); }); } + + #[test] fn capture_gap_keys_on_shell_channel_not_realized_fact_equality() { use crate::v1_compiler_emit::{shell_emission_refusal_fact, ShellEmissionRefusal}; use crate::v1_std_core::{make_error_node, CompilerDiagnostic}; From e8154cb8aade8784bedb65210492323717291cd0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 7 Oct 2026 21:24:21 +0000 Subject: [PATCH 22/27] Compose the #13466 gunbc-reach control with realized rust stderr capture. After the main merge the floor still required a capture-gap exclusion that this branch closed. Co-authored-by: Cursor --- src/v1/stage0/src/cli_run/emit_host.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 4d218fe11cb..8418e61a2b6 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -3701,8 +3701,9 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { ))) } } - // A fixture whose closure reaches extdeps.gunbc is admitted with the typed capture-policy - // exclusion, not as FixtureClosureUnionEmitRefused (the #13420 floor after #13437). + // A fixture whose closure reaches extdeps.gunbc is admitted by rust emit once + // WitnessBin.Run's capture channels are realized (#13472). #13466's typed exclusion + // remains for still-unrealized rust channels; it must not keep stripping Run. let gunbc_observed = fixture_closure_union_emit_receipt( &fixture_closure_union_control_union(FIXTURE_CLOSURE_GUNBC_REACH_MEMBER) .map_err(&refuse)?, @@ -3712,17 +3713,16 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { "a fixture whose closure reaches extdeps.gunbc refused: {refusal}" )) })?; - if !gunbc_observed.excluded.iter().any(|row| { + if gunbc_observed.excluded.iter().any(|row| { let Ok(gap) = capture_gap_exclusion() else { return false; }; row.contains(&format!("module={}", gap.declaring_module)) && row.contains(&format!("operation={}", gap.operation_qualified)) && row.contains("cause=ShellChannelNotRealizedByTarget") - && row.contains(&gap.drop_identity) }) { return Err(refuse(format!( - "a fixture whose closure reaches extdeps.gunbc was admitted without the typed exclusion: {gunbc_observed:?}" + "WitnessBin.Run is realized; the capture-gap exclusion must not still strip it: {gunbc_observed:?}" ))); } // A real emit error in a member the union still renders still refuses, even when the same From 370ee5aecfc0540ca96b3153d8caff96d6ea2f84 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 8 Oct 2026 18:11:46 +0000 Subject: [PATCH 23/27] Re-derive the #13466 gap-population tests after rust realizes capture channels. The constructed ShellChannelNotRealizedByTarget fact is unchanged; rust_stderr_capture_channel_not_realized_facts is empty because shell_channel_realized_by_target is now true for those channels. Install the required-regen emit_rust mirror after merging main. Co-authored-by: Cursor --- src/v1/stage0/src/cli_run/emit_host.rs | 20 +++++++++++++------- src/v1/stage0/src/v1_compiler_emit_rust.rs | 10 ++++++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 1460543261f..f76f3fe6127 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -4281,7 +4281,14 @@ mod fixture_closure_union_tests { target_name: "rust".to_string(), }, )); - assert!(stderr_capture_policy_gap_service(&mk(gap.clone())).is_some()); + // THE KEY STILL MATCHES THE REFUSAL RENDER. What emptied the population is + // `shell_channel_realized_by_target`: rust now realizes the three capture- + // accounting channels, so `rust_stderr_capture_channel_not_realized_facts` + // is empty and a constructed ShellChannelNotRealizedByTarget for + // stderr_truncated is outside the exclusion join. #13466's is_some was the + // gap-present control; after that realization the same fixture must stay + // unclassified as a gap (otherwise the union would strip a modeled Run). + assert!(stderr_capture_policy_gap_service(&mk(gap.clone())).is_none()); assert!(stderr_capture_policy_gap_service(&mk(unmodeled_key)).is_none()); assert!(stderr_capture_policy_gap_service(&mk(substring_poison)).is_none()); assert!(stderr_capture_policy_gap_service(&mk(stdout_unrealized)).is_none()); @@ -4342,13 +4349,12 @@ mod fixture_closure_union_tests { &gap_row.operation_qualified, gap.clone(), ); + // Same join as capture_gap_keys: the constructed gap fact is no longer a + // member of the unrealized-channel set, so the fold admits no exclusion. let selected = select_run_operations_excluded_for_stderr_capture_gap(&[allowed]); - assert_eq!( - selected.iter().cloned().collect::>(), - vec![( - gap_row.declaring_module.to_string(), - gap_row.service.to_string() - )] + assert!( + selected.is_empty(), + "rust-realized capture channels must not still select WitnessBin.Run for exclusion: {selected:?}" ); let mixed_key = crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index d6f8bdd894e..58e9b4913f3 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -20457,6 +20457,8 @@ pub fn emit_func_def( inferred.clone(), shared_types.clone(), scope.type_env.clone().source_indices.clone(), + emit_info.variant_to_enum.clone(), + scope.type_env.clone(), ); let body_scope = crate::v1_compiler_infer::build_params_scope( Rc::new(InferScope { @@ -20926,15 +20928,19 @@ pub fn emit_func_inferred( inferred: Rc, shared_types: Rc>, source_indices: Rc>>, + variant_to_enum: Rc>, + env: Rc, ) -> String { v1_rt::concat( v1_rt::concat( " -> Result<".to_string(), - render_rust_type( + render_rust_fn_sig_type( inferred.clone(), + Rc::new(vec![]), shared_types.clone(), source_indices.clone(), - crate::v1_compiler_infer_emit_info::empty_emit_graph_info(), + variant_to_enum.clone(), + env.clone(), ), ), ", Box>".to_string(), From f8d9cfe66ff270ab238c6f16c72c4de9f878c974 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 8 Oct 2026 18:15:05 +0000 Subject: [PATCH 24/27] Record that Complete's host-budget ceiling is generated rust_runtime_source. The seed-growth row already scoped the handler; review 77633 read the host-budget join as uncovered hand seed. Co-authored-by: Cursor --- .../rust_shell_stderr_capture_seed_growth.dag | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag index 9f9df6ab038..0d5da1ad8de 100644 --- a/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag +++ b/dag/gunbc/rust_shell_stderr_capture_seed_growth.dag @@ -26,8 +26,17 @@ import std.decl_ref { DeclarationRef, WholeDeclaration } // // Hand items below are the emit_host executing harness. The rust-item authority keeps // the inline-module path, so every citation is -// v1_compiler.cli_run.emit_host.fixture_closure_union_tests. The emitter body lives -// in src/v1/05_emit_rust.dag and its stage0 mirror; those are generated, not this roster. +// v1_compiler.cli_run.emit_host.fixture_closure_union_tests. +// +// THE COMPLETE CEILING IS NOT A SECOND HAND MODULE (review 77633). Complete overflow +// reads v1_rt::read_host_budget_bytes / resolve_host_budget_join. Those functions +// live in rust_runtime_source (runtime_rust.dag rt_host_budget) because an emitted +// crate has no memory_governor. The stage0 v1_rt.rs mirror is generated +// (gunbc.generated_artifact / derived_generated_stage0_repo_paths), so it is not a +// SeedGrowthJustification hand item and must not be listed here. memory_governor +// wraps the same join; this change deletes the second composer rather than adding +// one. The 2026-10-06 ruling's handler scope includes that ceiling: current_boundary +// already names WitnessStderrCaptureCompleteBudgetExceeded with the admitted limit. data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { hand_authored_declarations: [ DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "STDERR_CAPTURE_MEMBER", field: WholeDeclaration }, @@ -55,8 +64,8 @@ data rust_shell_stderr_capture_seed_growth_justification: SeedGrowthJustificatio DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_stdin_and_long_stderr_does_not_deadlock", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.emit_host.fixture_closure_union_tests", decl_name: "emitted_absent_policy_refuses_before_spawn", field: WholeDeclaration } ], - reason: "Operator ruling 2026-10-06 (msg_7853a1af): admit SeedFeatureCompletion, exact scope the rust shell handler realizing WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / stderr_retained_bytes) and its emit_host.rs tests. The rust shell handler must realize those channels so the fixture-closure union can emit WitnessBin.Run. These host tests compile and run the emitted drain against a child process, and they refuse a name-only, String, optional, or collection stderr_capture at TransportEmissionNotModeled. A .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", + reason: "Operator ruling 2026-10-06 (msg_7853a1af): admit SeedFeatureCompletion, exact scope the rust shell handler realizing WitnessStderrCapturePolicy (stderr_truncated / stderr_total_bytes / stderr_retained_bytes) and its emit_host.rs tests. The rust shell handler must realize those channels so the fixture-closure union can emit WitnessBin.Run. Complete's admitted limit is the existing host-budget join on rust_runtime_source (v1_rt::read_host_budget_bytes), not a new seed-retained module and not a second composer in memory_governor. These host tests compile and run the emitted drain against a child process, and they refuse a name-only, String, optional, or collection stderr_capture at TransportEmissionNotModeled. A .contains() on the drain string is not a consumer (DESIGN section 5). They stay rust because the subject is the rust realization fragment the seed emits.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, trigger: "Delete these items when the v2-native emitter realizes the shell transport's stderr capture, a .dag witness compiles the emitted rust crate and runs the same six discriminators plus the typed-policy emit refusals without a host rustc harness, and WitnessBin.Run still emits. The union digest_hex control stays.", - current_boundary: "Bind and validate a required scalar stderr_capture: WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and admitted limit. No fallback tail length. Capture-accounting channels without that typed required scalar refuse at the rust emit diagnostic (one shared predicate) and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." + current_boundary: "Bind and validate a required scalar stderr_capture: WitnessStderrCapturePolicy before spawn; start stdout and stderr drains immediately; write stdin concurrently; wait and join; project BoundedTail or return WitnessStderrCaptureCompleteBudgetExceeded with the measured total and the admitted limit from v1_rt::read_host_budget_bytes (rust_runtime_source / runtime_rust.dag rt_host_budget; memory_governor wraps the same join). No fallback tail length. Capture-accounting channels without that typed required scalar refuse at the rust emit diagnostic (one shared predicate) and, if that wall is skipped, as a pre-spawn return Err. Python and go still refuse those channels at emit. Unmodeled keys such as stderr_digest_hex still refuse." } From 5881f257fad93d48328b946390be884e31899924 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 8 Oct 2026 19:39:29 +0000 Subject: [PATCH 25/27] Retire the rust capture-gap drop now that the channels are realized. The restoration trigger fired with #13472: empty the exclusion, delete the union strip, and mark the drop Retired so the standing ledger matches the capability. Co-authored-by: Cursor --- ...closure_union_unmodeled_stderr_capture.dag | 37 +- dag/gunbc/stderr_capture_gap_exclusion.dag | 23 - docs/design-rung-drops.md | 6 +- src/v1/stage0/src/cli_run/emit_host.rs | 736 +----------------- 4 files changed, 26 insertions(+), 776 deletions(-) delete mode 100644 dag/gunbc/stderr_capture_gap_exclusion.dag diff --git a/dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag b/dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag index e9df5634113..0b70ff52377 100644 --- a/dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag +++ b/dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag @@ -1,29 +1,14 @@ module gunbc.rung_drop.fixture_closure_union_unmodeled_stderr_capture import std.types { NonEmptyStr } -import gunbc.rung_drop { RungDrop, Standing, TypedDeclaration, ReplacementStaged } +import gunbc.rung_drop { RungDrop, Retired, TypedDeclaration, ReplacementStaged } import gunbc.guarantee_rung { Mitigatable, MechanicallyPreventable } -import gunbc.stderr_capture_gap_exclusion { exclusion, StderrCaptureGapExclusion } - -// THE FIXTURE-CLOSURE UNION RENDERS EVERY RECORDED MEMBER THROUGH THE RUST EMITTER. After -// #13437 the walker closes by reference, so a fixture that reaches extdeps.gunbc compiles -// gunbc.WitnessBin.Run into the union. That operation declares stderr_truncated / -// stderr_total_bytes / stderr_retained_bytes, which v1.compiler.emit shell_channel_realized_by_target -// refuses for rust: the emitted Command body implements no WitnessStderrCapturePolicy. -// emit_artifact then returns no files for the WHOLE union, so those three diagnostics abort -// every other member's render and refuse unrelated floors (specimen: #13420). -// -// THE EARLIEST UNJUSTIFIED BOUNDARY IS THE UNION, NOT THE WALL. The wall is the honest -// capability gap (05_emit names the next-rung trigger: the emitted realization implementing -// the declared policy). std.shell_stream_capture still homes the policy on the interpreter -// drain until that trigger fires. Binding a fabricating rust handler (truncated=false) is the -// fail-open 05_emit already refuses. So the union must not render the unmodeled operation: -// a typed exclusion keyed on that diagnostic fact, never a silent skip, and a real emit error -// in a member the union still renders still refuses. - -fn capture_gap_exclusion_population() -> StderrCaptureGapExclusion { - exclusion -} + +// RETIRED — TRIGGER FIRED. gunbc#13472 realized rust shell WitnessStderrCapturePolicy +// (v1.compiler.emit shell_channel_realized_by_target is true for the three capture- +// accounting channels). The union no longer strips WitnessBin.Run. Discriminating +// controls stay enrolled: fixture_closure_union_controls gunbc-reach emit, plus +// emit_host realization tests (DESIGN §4b(4)). data fixture_closure_union_unmodeled_stderr_capture: RungDrop = RungDrop { identity: "fixture_closure_union_unmodeled_stderr_capture" as NonEmptyStr, @@ -32,7 +17,9 @@ data fixture_closure_union_unmodeled_stderr_capture: RungDrop = RungDrop { declared: "2026-10-06", - standing: Standing, + standing: Retired { + trigger_fired: "2026-10-08 -- gunbc#13472. THE CAPABILITY, EXECUTED: v1.compiler.emit shell_channel_realized_by_target returns true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes on rust because emit_shell_call implements the declared WitnessStderrCapturePolicy. WitnessBin.Run emits without TransportEmissionNotModeled; the union strip of that service is deleted; this exclusion population is empty. DISCRIMINATING CONTROL, ENROLLED: v1_compiler.cli_run fixture_closure_union_controls admits a fixture whose closure reaches extdeps.gunbc, and a real emit error beside that reach still refuses as FixtureClosureUnionEmitRefused at module=efr_member. Answering the channels as untruncated whole-stream lengths did not discharge this. Deleting the channels from the interface did not discharge this." as NonEmptyStr + }, declaration: TypedDeclaration { previous: MechanicallyPreventable, @@ -41,9 +28,7 @@ data fixture_closure_union_unmodeled_stderr_capture: RungDrop = RungDrop { reason: ReplacementStaged { replacement: "the rust emitted realization implementing the declared WitnessStderrCapturePolicy, the same next-rung trigger v1.compiler.emit shell_channel_realized_by_target already names" }, - population: [ - "exactly gunbc.stderr_capture_gap_exclusion.exclusion (consumed here as capture_gap_exclusion_population): gunbc.WitnessBin.Run in extdeps.gunbc. Output channels stderr_truncated, stderr_total_bytes, stderr_retained_bytes, each a TransportEmissionNotModeled ShellChannelNotRealizedByTarget for target rust. A later shell operation with the same capture-gap fact is NOT a member: the union still renders it and still refuses. The union compiles and evaluates that record; deleting either this module or the carrier refuses the seed compile", - ], + population: [], restoration_trigger: "THE CAPABILITY: the rust emitted shell realization implements the operation's declared WitnessStderrCapturePolicy, sufficient for v1.compiler.emit shell_channel_realized_by_target to return true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes. WHAT THAT MUST BE SUFFICIENT FOR: those operations emit for rust without TransportEmissionNotModeled, this exclusion has an empty population and this row and the union's strip of those services delete together. Answering the channels as untruncated whole-stream lengths does NOT discharge it. Deleting the channels from the interface does NOT discharge it. Silently omitting the module from the union without naming this cause does NOT discharge it.", } diff --git a/dag/gunbc/stderr_capture_gap_exclusion.dag b/dag/gunbc/stderr_capture_gap_exclusion.dag deleted file mode 100644 index 55cda509ef8..00000000000 --- a/dag/gunbc/stderr_capture_gap_exclusion.dag +++ /dev/null @@ -1,23 +0,0 @@ -module gunbc.stderr_capture_gap_exclusion - -// THE FIXTURE-CLOSURE UNION'S EXCLUDED OPERATION, as one record the seed compiles and evaluates. -// gunbc.rung_drop.fixture_closure_union_unmodeled_stderr_capture consumes this row. Authored names -// are the compiler's declaration identity for extdeps.gunbc / WitnessBin / Run (RungDrop.population -// stays List for every other drop; this carrier is the typed field that row cannot grow -// without migrating the ledger). - -type StderrCaptureGapExclusion { - drop_identity: String - declaring_module: String - service: String - operation_qualified: String - operation_bare: String -} - -data exclusion: StderrCaptureGapExclusion = StderrCaptureGapExclusion { - drop_identity: "gunbc.rung_drop.fixture_closure_union_unmodeled_stderr_capture", - declaring_module: "extdeps.gunbc", - service: "gunbc.WitnessBin", - operation_qualified: "gunbc.WitnessBin.Run", - operation_bare: "Run", -} diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index 7d2c709ff07..8f9c248422e 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -142,9 +142,11 @@ an append to the fabric event log by a principal that is not a fleet writer: RUN per-module rust emit refusals of corpus modules reachable from required-floor fixture closures, outside the v1 seed closure: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of gunbc#13037 (floor C1): compile_dag_rust_emit_check and compile_dag_diagnostic_census stopped rendering every closure module (RenderEveryModule -> v1_compiler.cli_run fixture_render_selection)). Population: every corpus module in the transitive import closure of a fixture source passed to compile_dag_rust_emit_check or compile_dag_diagnostic_census on the required floor, that is not a v1 seed module (no `// Source module:` header under src/v1/stage0/src) or is a seed mirror that the affected-set bound (v1_compiler.required_regen_host scope_selection) does not select on the run in question. Restored when: every corpus module reachable from a required-floor fixture closure is rendered at least once per required run through the rust emitter, and a per-module emit refusal refuses the floor. -### fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize — declared 2026-10-06 +### fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize — declared 2026-10-06 · RETIRED -fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the rust emitted realization implementing the declared WitnessStderrCapturePolicy, the same next-rung trigger v1.compiler.emit shell_channel_realized_by_target already names). Population: exactly gunbc.stderr_capture_gap_exclusion.exclusion (consumed here as capture_gap_exclusion_population): gunbc.WitnessBin.Run in extdeps.gunbc. Output channels stderr_truncated, stderr_total_bytes, stderr_retained_bytes, each a TransportEmissionNotModeled ShellChannelNotRealizedByTarget for target rust. A later shell operation with the same capture-gap fact is NOT a member: the union still renders it and still refuses. The union compiles and evaluates that record; deleting either this module or the carrier refuses the seed compile. Restored when: THE CAPABILITY: the rust emitted shell realization implements the operation's declared WitnessStderrCapturePolicy, sufficient for v1.compiler.emit shell_channel_realized_by_target to return true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes. WHAT THAT MUST BE SUFFICIENT FOR: those operations emit for rust without TransportEmissionNotModeled, this exclusion has an empty population and this row and the union's strip of those services delete together. Answering the channels as untruncated whole-stream lengths does NOT discharge it. Deleting the channels from the interface does NOT discharge it. Silently omitting the module from the union without naming this cause does NOT discharge it. +**RETIRED — TRIGGER FIRED.** 2026-10-08 -- gunbc#13472. THE CAPABILITY, EXECUTED: v1.compiler.emit shell_channel_realized_by_target returns true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes on rust because emit_shell_call implements the declared WitnessStderrCapturePolicy. WitnessBin.Run emits without TransportEmissionNotModeled; the union strip of that service is deleted; this exclusion population is empty. DISCRIMINATING CONTROL, ENROLLED: v1_compiler.cli_run fixture_closure_union_controls admits a fixture whose closure reaches extdeps.gunbc, and a real emit error beside that reach still refuses as FixtureClosureUnionEmitRefused at module=efr_member. Answering the channels as untruncated whole-stream lengths did not discharge this. Deleting the channels from the interface did not discharge this. + +fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the rust emitted realization implementing the declared WitnessStderrCapturePolicy, the same next-rung trigger v1.compiler.emit shell_channel_realized_by_target already names). Population: . Restored when: THE CAPABILITY: the rust emitted shell realization implements the operation's declared WitnessStderrCapturePolicy, sufficient for v1.compiler.emit shell_channel_realized_by_target to return true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes. WHAT THAT MUST BE SUFFICIENT FOR: those operations emit for rust without TransportEmissionNotModeled, this exclusion has an empty population and this row and the union's strip of those services delete together. Answering the channels as untruncated whole-stream lengths does NOT discharge it. Deleting the channels from the interface does NOT discharge it. Silently omitting the module from the union without naming this cause does NOT discharge it. ### new-witness eval-step cost gate over the one claim that runs the real fleet-converge build-job step list to hold the reset-observer dispatch admission's membership: it still executes, eval_steps stay recorded, a semantic red still blocks; only the eval-step cost-gate rung is lowered — declared 2026-09-30 diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index f76f3fe6127..6182963a91d 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -51,7 +51,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, OnceLock, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use crate::coproduct_reflection::{decl_facts_corpus_walk, DeclFactRaw}; use crate::module_path_index::{ @@ -3123,10 +3123,9 @@ pub(crate) fn fixture_closure_union_digest(members: &BTreeMap) - /// is the compile's list followed by the emitter's (`emit_resolved_for_target_selected`), so /// the suffix past the compile's length is exactly what the emitter added. /// -/// Unmodeled rust stderr-capture channels (`gunbc.rung_drop` -/// `fixture_closure_union_unmodeled_stderr_capture`) are a typed exclusion, not this refusal: -/// those services are stripped from the emit graph and listed on the observation. A sibling -/// unmodeled key or any other emit error still refuses. +/// Rust realizes the three capture-accounting channels. An unmodeled sibling key or any +/// other emit error still refuses. The retired drop +/// `fixture_closure_union_unmodeled_stderr_capture` no longer strips a service. pub(crate) fn fixture_closure_union_emit_receipt( union: &FixtureClosureUnion, ) -> Result { @@ -3198,7 +3197,6 @@ pub(crate) fn fixture_closure_union_emit_receipt( ), )); } - let (resolved, excluded) = union_emit_graph_excluding_unmodeled_stderr_capture(resolved); let compile_diagnostics = resolved.diagnostics.len(); let rendered = v1_compiler_compile::emit_resolved_for_target( resolved, @@ -3229,369 +3227,10 @@ pub(crate) fn fixture_closure_union_emit_receipt( digest, files: rendered.files.len(), emit_diagnostics: emitted.len(), - excluded, + excluded: Vec::new(), }) } -/// Deleting either the drop row or its typed carrier fails this compile. The union compiles -/// the carrier through `compile_to_resolved` and evaluates `exclusion` (review 77125). -const STDERR_CAPTURE_GAP_CARRIER_SOURCE: &str = - include_str!("../../../../../dag/gunbc/stderr_capture_gap_exclusion.dag"); -const STDERR_CAPTURE_POLICY_DROP_SOURCE: &str = include_str!( - "../../../../../dag/gunbc/rung_drop/fixture_closure_union_unmodeled_stderr_capture.dag" -); - -struct CaptureGapExclusion { - drop_identity: String, - declaring_module: String, - service: String, - operation_qualified: String, - operation_bare: String, -} - -fn record_field_string( - ctx: &crate::v1_interpreter::InterpContext, - fields: &[(crate::v1_interpreter::Symbol, crate::v1_interpreter::Value)], - name: &str, -) -> Result { - use crate::v1_interpreter::Value; - match fields - .iter() - .find(|(sym, _)| ctx.sym_eq(*sym, name)) - .map(|(_, v)| v) - { - Some(Value::Str(s)) => Ok(s.to_string()), - Some(other) => Err(format!( - "cause=CaptureGapExclusionFieldNotString field={name} value={other:?}" - )), - None => Err(format!( - "cause=CaptureGapExclusionFieldMissing field={name}" - )), - } -} - -fn load_capture_gap_exclusion() -> Result { - let _drop_row_tether = STDERR_CAPTURE_POLICY_DROP_SOURCE; - let files = vec![Rc::new(v1_compiler_compile::SourceFile { - path: "dag/gunbc/stderr_capture_gap_exclusion.dag".to_string(), - content: STDERR_CAPTURE_GAP_CARRIER_SOURCE.to_string(), - })]; - let resolved = v1_compiler_compile::compile_to_resolved(Rc::new(files.into())); - let located = |d: &Rc| { - format!( - "module={} reason=`{}`", - d.module_name, - crate::v1_std_core::diagnostic_to_message(d.diagnostic.clone()) - ) - }; - if v1_compiler_compile::emittable_graph(resolved.clone()).is_none() { - let blocking: Vec = resolved - .diagnostics - .iter() - .filter(|d| { - crate::v1_std_core::is_interpreter_blocking_diagnostic(d.diagnostic.clone()) - }) - .map(located) - .collect(); - return Err(format!( - "cause=CaptureGapExclusionUncompilable drop_row_bytes={} diagnostics={}", - _drop_row_tether.len(), - blocking.join(" | ") - )); - } - let Some(graph) = resolved.graph.clone() else { - return Err(format!( - "cause=CaptureGapExclusionUncompilable drop_row_bytes={} -- resolved graph is none", - _drop_row_tether.len() - )); - }; - let ctx = make_eval_context( - &graph, - resolved.source_indices.clone(), - crate::v1_interpreter::ExecutionMode::Hermetic, - ); - let val = crate::v1_interpreter::with_active_context(&ctx, || { - match crate::v1_interpreter::eval_data_item_value(&ctx, "exclusion") { - Ok(Some(v)) => Ok(Some(v)), - Ok(None) => crate::v1_interpreter::eval_data_item_value( - &ctx, - "gunbc.stderr_capture_gap_exclusion.exclusion", - ), - Err(e) => Err(e), - } - }) - .map_err(|e| format!("cause=CaptureGapExclusionEvalFailed error={e}"))? - .ok_or_else(|| "cause=CaptureGapExclusionDataMissing name=exclusion".to_string())?; - let crate::v1_interpreter::Value::Record { - ref fields, - type_name, - } = val - else { - return Err(format!("cause=CaptureGapExclusionNotRecord value={val:?}")); - }; - if !ctx.sym_eq(type_name, "StderrCaptureGapExclusion") - && !ctx.sym_eq( - type_name, - "gunbc.stderr_capture_gap_exclusion.StderrCaptureGapExclusion", - ) - { - return Err(format!( - "cause=CaptureGapExclusionUnexpectedType type={}", - ctx.resolve(type_name) - )); - } - Ok(CaptureGapExclusion { - drop_identity: record_field_string(&ctx, fields.as_slice(), "drop_identity")?, - declaring_module: record_field_string(&ctx, fields.as_slice(), "declaring_module")?, - service: record_field_string(&ctx, fields.as_slice(), "service")?, - operation_qualified: record_field_string(&ctx, fields.as_slice(), "operation_qualified")?, - operation_bare: record_field_string(&ctx, fields.as_slice(), "operation_bare")?, - }) -} - -fn capture_gap_exclusion() -> Result<&'static CaptureGapExclusion, &'static str> { - static GAP: OnceLock> = OnceLock::new(); - match GAP.get_or_init(load_capture_gap_exclusion) { - Ok(gap) => Ok(gap), - Err(e) => Err(e.as_str()), - } -} - -/// Facts `v1.compiler.emit` `shell_emission_refusal_fact` renders for -/// `ShellChannelNotRealizedByTarget` on the three stderr-accounting channels the drop names. -/// Not every channel `shell_channel_realized_by_target` currently returns false for: a later -/// unrealized stdout-side channel must still refuse the union (review 77034). -fn rust_stderr_capture_channel_not_realized_facts() -> &'static BTreeSet { - static FACTS: OnceLock> = OnceLock::new(); - FACTS.get_or_init(|| { - use crate::v1_compiler_artifact::RenderTarget; - use crate::v1_compiler_emit::{ - render_target_name, shell_channel_realized_by_target, shell_emission_refusal_fact, - shell_result_channel_key, ShellEmissionRefusal, ShellResultChannel, - }; - let target = RenderTarget::Rust; - let target_name = render_target_name(target); - [ - ShellResultChannel::ShellChanStderrTruncated, - ShellResultChannel::ShellChanStderrTotalBytes, - ShellResultChannel::ShellChanStderrRetainedBytes, - ] - .into_iter() - .filter(|channel| !shell_channel_realized_by_target(*channel, target)) - .map(|channel| { - shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: shell_result_channel_key(channel), - target_name: target_name.clone(), - }, - )) - }) - .collect() - }) -} - -/// The drop population: rust shell TransportEmissionNotModeled on exactly -/// `extdeps.gunbc` `WitnessBin.Run` whose fact equals a ShellChannelNotRealizedByTarget -/// fact for an unrealized rust channel. Any other module, service, or operation stays rendered. -fn stderr_capture_policy_gap_service(d: &Rc) -> Option<(String, String)> { - let Ok(gap) = capture_gap_exclusion() else { - return None; - }; - match &*d.diagnostic { - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind, - service, - operation, - declaring_module, - target, - missing_realization_fact, - .. - } if transport_kind == "shell" - && target == "rust" - && declaring_module.as_str() == gap.declaring_module - && service.as_str() == gap.service - && is_drop_run_operation(operation) - && rust_stderr_capture_channel_not_realized_facts() - .contains(missing_realization_fact) => - { - Some((declaring_module.clone(), service.clone())) - } - _ => None, - } -} - -fn is_drop_run_operation(operation: &str) -> bool { - let Ok(gap) = capture_gap_exclusion() else { - return false; - }; - operation == gap.operation_bare || operation == gap.operation_qualified -} - -fn transport_emission_run(d: &Rc) -> Option<(String, String)> { - match &*d.diagnostic { - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - service, - operation, - declaring_module, - .. - } if is_drop_run_operation(operation) => Some((declaring_module.clone(), service.clone())), - _ => None, - } -} - -/// The selection fold: among supplied unmodeled-transport rows, keep `(module, service)` -/// only when every Run row for that pair is a capture-gap fact. Production feeds it the -/// three emit diagnostic streams; tests supply rows. -fn select_run_operations_excluded_for_stderr_capture_gap( - unmodeled: &[Rc], -) -> BTreeSet<(String, String)> { - let mut gap_runs: BTreeSet<(String, String)> = BTreeSet::new(); - for d in unmodeled { - if let Some(key) = stderr_capture_policy_gap_service(d) { - gap_runs.insert(key); - } - } - gap_runs - .into_iter() - .filter(|key| { - unmodeled - .iter() - .filter(|d| transport_emission_run(d).as_ref() == Some(key)) - .all(|d| stderr_capture_policy_gap_service(d).is_some()) - }) - .collect() -} - -/// `extdeps.gunbc` `gunbc.WitnessBin.Run` when that operation's unmodeled-transport refusals -/// are solely the rust stderr-capture gap. Other operations on the same service are not members. -fn run_operations_excluded_for_stderr_capture_gap( - typed: &Rc, -) -> BTreeSet<(String, String)> { - let target = crate::v1_compiler_artifact::RenderTarget::Rust; - let mut unmodeled = Vec::new(); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_file_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_shell_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - unmodeled.extend( - crate::v1_compiler_emit::unmodeled_rest_transport_diagnostics(typed.clone(), target) - .iter() - .cloned(), - ); - select_run_operations_excluded_for_stderr_capture_gap(&unmodeled) -} - -fn strip_excluded_run_operations( - typed: Rc, - excluded: &BTreeSet<(String, String)>, -) -> Rc { - if excluded.is_empty() { - return typed; - } - let modules = Rc::new( - typed - .modules - .iter() - .map(|tm| { - let module_name = crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - tm.module.clone(), - ); - let items = Rc::new( - tm.items - .iter() - .filter_map(|item| { - if item.module_item_kind - != crate::v1_std_core::ParsedModuleItemKind::ModuleItemService - { - return Some((*item).clone()); - } - let service = crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - (*item).clone(), - ); - if !excluded.contains(&(module_name.clone(), service)) { - return Some((*item).clone()); - } - let kept: im::Vector<_> = item - .children - .iter() - .filter(|op| { - !is_drop_run_operation( - &crate::v1_compiler_infer_env::authored_name( - tm.type_env.clone(), - (*op).clone(), - ), - ) - }) - .cloned() - .collect(); - if kept.is_empty() { - return None; - } - Some(Rc::new({ - let mut node = (**item).clone(); - node.children = Rc::new(kept); - node - })) - }) - .collect::>(), - ); - Rc::new(crate::v1_compiler_infer_items::TypedModule { - items, - ..(**tm).clone() - }) - }) - .collect::>(), - ); - Rc::new(crate::v1_compiler_infer_items::ResolvedGraph { - modules, - ..(*typed).clone() - }) -} - -/// Compile stays the full closure. Emit strips only `gunbc.WitnessBin.Run` when its rust -/// refusals are solely the capture-policy gap. Other operations on that service stay. -fn union_emit_graph_excluding_unmodeled_stderr_capture( - resolved: Rc, -) -> (Rc, Vec) { - let Some(typed) = resolved.graph.clone() else { - return (resolved, Vec::new()); - }; - let excluded_keys = run_operations_excluded_for_stderr_capture_gap(&typed); - if excluded_keys.is_empty() { - return (resolved, Vec::new()); - } - let Ok(gap) = capture_gap_exclusion() else { - return (resolved, Vec::new()); - }; - let excluded: Vec = excluded_keys - .iter() - .map(|(module, service)| { - format!( - "module={module} service={service} operation={} \ - cause=ShellChannelNotRealizedByTarget \ - fact=stderr_capture_policy_unrealized drop={}", - gap.operation_qualified, gap.drop_identity, - ) - }) - .collect(); - let graph = strip_excluded_run_operations(typed, &excluded_keys); - ( - Rc::new(v1_compiler_compile::ResolvedPipelineResult { - graph: Some(graph), - ..(*resolved).clone() - }), - excluded, - ) -} - /// The red control's member: a non-tail effectful self-call, which the rust emitter refuses /// (`EffectfulSelfRecursionUnrealized`, the derived form exercised by /// `test.claim.effectful_item_kind_collapse_witness_test`). @@ -3708,9 +3347,8 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { ))) } } - // A fixture whose closure reaches extdeps.gunbc is admitted by rust emit once - // WitnessBin.Run's capture channels are realized (#13472). #13466's typed exclusion - // remains for still-unrealized rust channels; it must not keep stripping Run. + // A fixture whose closure reaches extdeps.gunbc is admitted by rust emit: WitnessBin.Run + // is realized, and the retired capture-gap strip is gone (review 77726). let gunbc_observed = fixture_closure_union_emit_receipt( &fixture_closure_union_control_union(FIXTURE_CLOSURE_GUNBC_REACH_MEMBER) .map_err(&refuse)?, @@ -3720,20 +3358,13 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { "a fixture whose closure reaches extdeps.gunbc refused: {refusal}" )) })?; - if gunbc_observed.excluded.iter().any(|row| { - let Ok(gap) = capture_gap_exclusion() else { - return false; - }; - row.contains(&format!("module={}", gap.declaring_module)) - && row.contains(&format!("operation={}", gap.operation_qualified)) - && row.contains("cause=ShellChannelNotRealizedByTarget") - }) { + if !gunbc_observed.excluded.is_empty() { return Err(refuse(format!( - "WitnessBin.Run is realized; the capture-gap exclusion must not still strip it: {gunbc_observed:?}" + "the retired capture-gap strip must not still exclude a member: {gunbc_observed:?}" ))); } // A real emit error in a member the union still renders still refuses, even when the same - // closure also reaches the excluded service. + // closure also reaches extdeps.gunbc. match fixture_closure_union_emit_receipt( &fixture_closure_union_control_union(FIXTURE_CLOSURE_GUNBC_AND_REAL_EMIT_ERROR) .map_err(&refuse)?, @@ -3748,7 +3379,7 @@ pub(crate) fn fixture_closure_union_controls() -> Result<(u128, u128), String> { } Ok(observed) => { return Err(refuse(format!( - "a real emit error beside the excluded gunbc service did not refuse the union: {observed:?}" + "a real emit error beside a gunbc-reaching closure did not refuse the union: {observed:?}" ))) } } @@ -3778,10 +3409,6 @@ mod fixture_closure_union_tests { /// The recorder and the union are process-wide; tests that touch them run one at a time. static UNION_TEST_LOCK: Mutex<()> = Mutex::new(()); - fn gap_excl() -> &'static CaptureGapExclusion { - capture_gap_exclusion().unwrap_or_else(|e| panic!("{e}")) - } - /// An empty union is a bypassed recording seam and refuses (review 76399). #[test] fn an_empty_union_refuses() { @@ -4242,347 +3869,6 @@ mod fixture_closure_union_tests { panic!("a compile-probe reach of extdeps.gunbc must emit: {refusal}"); }); } - - #[test] - fn capture_gap_keys_on_shell_channel_not_realized_fact_equality() { - use crate::v1_compiler_emit::{shell_emission_refusal_fact, ShellEmissionRefusal}; - use crate::v1_std_core::{make_error_node, CompilerDiagnostic}; - let span = crate::v1_std_core::kernel_span("probe".to_string()); - let mk = |fact: String| { - let gap = gap_excl(); - make_error_node( - Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: gap.service.to_string(), - operation: gap.operation_qualified.to_string(), - declaring_module: gap.declaring_module.to_string(), - target: "rust".to_string(), - missing_realization_fact: fact, - span: span.clone(), - }), - gap.declaring_module.to_string(), - ) - }; - let gap = shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stderr_truncated".to_string(), - target_name: "rust".to_string(), - }, - )); - let unmodeled_key = - shell_emission_refusal_fact(Rc::new(ShellEmissionRefusal::ShellOutputKeyNotModeled { - key: "not_a_channel".to_string(), - })); - let substring_poison = - "unmodeled key 'not_a_channel' implements no stderr capture policy".to_string(); - let stdout_unrealized = shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stdout".to_string(), - target_name: "rust".to_string(), - }, - )); - // THE KEY STILL MATCHES THE REFUSAL RENDER. What emptied the population is - // `shell_channel_realized_by_target`: rust now realizes the three capture- - // accounting channels, so `rust_stderr_capture_channel_not_realized_facts` - // is empty and a constructed ShellChannelNotRealizedByTarget for - // stderr_truncated is outside the exclusion join. #13466's is_some was the - // gap-present control; after that realization the same fixture must stay - // unclassified as a gap (otherwise the union would strip a modeled Run). - assert!(stderr_capture_policy_gap_service(&mk(gap.clone())).is_none()); - assert!(stderr_capture_policy_gap_service(&mk(unmodeled_key)).is_none()); - assert!(stderr_capture_policy_gap_service(&mk(substring_poison)).is_none()); - assert!(stderr_capture_policy_gap_service(&mk(stdout_unrealized)).is_none()); - let other_module = make_error_node( - Rc::new(CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: gap_excl().service.to_string(), - operation: gap_excl().operation_qualified.to_string(), - declaring_module: "extdeps.other".to_string(), - target: "rust".to_string(), - missing_realization_fact: gap, - span: span.clone(), - }), - "extdeps.other".to_string(), - ); - assert!(stderr_capture_policy_gap_service(&other_module).is_none()); - } - - fn gap_fact() -> String { - use crate::v1_compiler_emit::{shell_emission_refusal_fact, ShellEmissionRefusal}; - shell_emission_refusal_fact(Rc::new( - ShellEmissionRefusal::ShellChannelNotRealizedByTarget { - key: "stderr_truncated".to_string(), - target_name: "rust".to_string(), - }, - )) - } - - fn transport_row( - module: &str, - service: &str, - operation: &str, - fact: String, - ) -> Rc { - crate::v1_std_core::make_error_node( - Rc::new( - crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { - transport_kind: "shell".to_string(), - service: service.to_string(), - operation: operation.to_string(), - declaring_module: module.to_string(), - target: "rust".to_string(), - missing_realization_fact: fact, - span: crate::v1_std_core::kernel_span("probe".to_string()), - }, - ), - module.to_string(), - ) - } - - #[test] - fn selection_fold_allows_only_run_capture_rows_and_vetoes_mixed_refusal() { - let gap = gap_fact(); - let gap_row = gap_excl(); - let allowed = transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), - ); - // Same join as capture_gap_keys: the constructed gap fact is no longer a - // member of the unrealized-channel set, so the fold admits no exclusion. - let selected = select_run_operations_excluded_for_stderr_capture_gap(&[allowed]); - assert!( - selected.is_empty(), - "rust-realized capture channels must not still select WitnessBin.Run for exclusion: {selected:?}" - ); - - let mixed_key = crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( - crate::v1_compiler_emit::ShellEmissionRefusal::ShellOutputKeyNotModeled { - key: "not_a_channel".to_string(), - }, - )); - let mixed = vec![ - transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), - ), - transport_row( - &gap_row.declaring_module, - &gap_row.service, - &gap_row.operation_qualified, - mixed_key, - ), - ]; - assert!(select_run_operations_excluded_for_stderr_capture_gap(&mixed).is_empty()); - - let wrong_module = transport_row( - "extdeps.other", - &gap_row.service, - &gap_row.operation_qualified, - gap.clone(), - ); - assert!(select_run_operations_excluded_for_stderr_capture_gap(&[wrong_module]).is_empty()); - - let nonmember = transport_row(&gap_row.declaring_module, &gap_row.service, "Sibling", gap); - assert!(select_run_operations_excluded_for_stderr_capture_gap(&[nonmember]).is_empty()); - } - - fn kernel_named( - name: &str, - kind: crate::v1_std_core::ParsedModuleItemKind, - children: im::Vector>, - ) -> Rc { - let mut node = (*crate::v1_std_core::leaf_node_with_span( - Rc::new(crate::std_occurrence_identity::NodeOccurrenceIdentity::OccurrenceSynthetic), - name.to_string(), - crate::v1_std_core::kernel_span(name.to_string()), - )) - .clone(); - node.module_item_kind = kind; - node.children = Rc::new(children); - Rc::new(node) - } - - fn supplied_graph_with_run_and_sibling() -> Rc { - use crate::v1_compiler_infer_items::{ - ModuleInterface, ModuleTypecheckProgress, ResolvedGraph, TypedModule, - }; - use crate::v1_std_core::ParsedModuleItemKind; - let env = crate::v1_compiler_infer_env::empty_type_env(); - let cache = crate::v1_compiler_infer_env::empty_type_env_cache(); - let gap = gap_excl(); - let run = kernel_named( - &gap.operation_bare, - ParsedModuleItemKind::NotAModuleItem, - im::vector![], - ); - let sibling = kernel_named( - "Sibling", - ParsedModuleItemKind::NotAModuleItem, - im::vector![], - ); - let other_fn = kernel_named( - "unrelated_fn", - ParsedModuleItemKind::ModuleItemFunction, - im::vector![], - ); - let service = kernel_named( - &gap.service, - ParsedModuleItemKind::ModuleItemService, - im::vector![run, sibling], - ); - let module = kernel_named( - &gap.declaring_module, - ParsedModuleItemKind::NotAModuleItem, - im::vector![], - ); - let other_module_node = kernel_named( - "other.mod", - ParsedModuleItemKind::NotAModuleItem, - im::vector![], - ); - let other_item = kernel_named( - "KeepMe", - ParsedModuleItemKind::ModuleItemFunction, - im::vector![], - ); - let interface = |e: Rc, - c: Rc, - path: &str| { - Rc::new(ModuleInterface { - summary: Rc::new(crate::std_interface_summary::InterfaceSummary { - module_path: path.to_string(), - exports: Rc::new(im::vector![]), - interface_hash: crate::std_interface_summary::interface_summary_rollup( - Rc::new(im::vector![]), - ), - }), - env: e, - cache: c, - }) - }; - let tm = Rc::new(TypedModule { - module, - items: Rc::new(im::vector![service, other_fn]), - progress: ModuleTypecheckProgress::ItemsChecked, - type_env: env.clone(), - type_env_cache: cache.clone(), - interface: interface(env.clone(), cache.clone(), &gap.declaring_module), - func_env: Rc::new(crate::v1_compiler_infer_sigs::ResolvedFuncEnv { - name: gap.declaring_module.to_string(), - local: crate::v1_rt::rc_empty_map(), - parents: Rc::new(im::vector![]), - }), - item_registry: crate::v1_rt::rc_empty_map(), - occurrence_transport: None, - }); - let other = Rc::new(TypedModule { - module: other_module_node, - items: Rc::new(im::vector![other_item]), - progress: ModuleTypecheckProgress::ItemsChecked, - type_env: env.clone(), - type_env_cache: cache.clone(), - interface: interface(env, cache, "other.mod"), - func_env: Rc::new(crate::v1_compiler_infer_sigs::ResolvedFuncEnv { - name: "other.mod".to_string(), - local: crate::v1_rt::rc_empty_map(), - parents: Rc::new(im::vector![]), - }), - item_registry: crate::v1_rt::rc_empty_map(), - occurrence_transport: None, - }); - Rc::new(ResolvedGraph { - modules: Rc::new(im::vector![tm, other]), - item_registry: crate::v1_rt::rc_empty_map(), - item_leaf_owner_modules: crate::v1_rt::rc_empty_map(), - diagnostics: Rc::new(im::vector![]), - }) - } - - fn service_op_names( - graph: &crate::v1_compiler_infer_items::ResolvedGraph, - module: &str, - service: &str, - ) -> Vec { - let env = crate::v1_compiler_infer_env::empty_type_env(); - graph - .modules - .iter() - .find(|tm| { - crate::v1_compiler_infer_env::authored_name(tm.type_env.clone(), tm.module.clone()) - == module - }) - .into_iter() - .flat_map(|tm| tm.items.iter()) - .filter(|item| { - item.module_item_kind == crate::v1_std_core::ParsedModuleItemKind::ModuleItemService - && crate::v1_compiler_infer_env::authored_name(env.clone(), (*item).clone()) - == service - }) - .flat_map(|item| { - item.children - .iter() - .map(|op| crate::v1_compiler_infer_env::authored_name(env.clone(), op.clone())) - }) - .collect() - } - - #[test] - fn strip_removes_only_run_and_keeps_sibling_and_unrelated_items() { - let gap = gap_excl(); - let graph = supplied_graph_with_run_and_sibling(); - let mut excluded = BTreeSet::new(); - excluded.insert((gap.declaring_module.to_string(), gap.service.to_string())); - let stripped = strip_excluded_run_operations(graph.clone(), &excluded); - assert_eq!( - service_op_names(&stripped, &gap.declaring_module, &gap.service), - vec!["Sibling".to_string()] - ); - let env = crate::v1_compiler_infer_env::empty_type_env(); - let gunbc_item_names: Vec = stripped - .modules - .iter() - .find(|tm| { - crate::v1_compiler_infer_env::authored_name(tm.type_env.clone(), tm.module.clone()) - == gap.declaring_module - }) - .unwrap() - .items - .iter() - .map(|item| crate::v1_compiler_infer_env::authored_name(env.clone(), item.clone())) - .collect(); - assert!( - gunbc_item_names.contains(&"unrelated_fn".to_string()), - "{gunbc_item_names:?}" - ); - assert_eq!(stripped.modules.len(), 2, "unrelated module must remain"); - let empty = strip_excluded_run_operations(graph, &BTreeSet::new()); - assert_eq!( - service_op_names(&empty, &gap.declaring_module, &gap.service), - vec![gap.operation_bare.to_string(), "Sibling".to_string()] - ); - } - - #[test] - fn capture_gap_exclusion_reads_typed_fields_from_the_drop_module() { - let gap = gap_excl(); - assert!(!gap.drop_identity.is_empty()); - assert!(!gap.declaring_module.is_empty()); - assert!(!gap.service.is_empty()); - assert!(!gap.operation_bare.is_empty()); - assert!( - gap.operation_qualified == gap.operation_bare - || gap - .operation_qualified - .ends_with(&format!(".{}", gap.operation_bare)), - "qualified={} bare={}", - gap.operation_qualified, - gap.operation_bare - ); - } } #[cfg(test)] From 5d9a72d0be5eabaa3edc9ee5d942be074486e85f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Fri, 9 Oct 2026 01:30:20 +0000 Subject: [PATCH 26/27] walk_cgroup, entry_presence: put the self-recursion in tail position so both modules rust-emit The floor's union emit refuses both with EffectfulSelfRecursionUnrealized (non-tail async recursion has no realization). entry_presence walks ancestors through entry_presence_above, carrying the cause read one level down; walk_cgroup walks a preorder pending frontier through walk_cgroup_pending. Results and refusal arms are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/gunbc/live_deploy/member_observe.dag | 19 +++++++---- .../runner_microvm_lifecycle_realize.dag | 34 +++++++++++++------ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/dag/gunbc/live_deploy/member_observe.dag b/dag/gunbc/live_deploy/member_observe.dag index 76ada9499ed..0dd3e0dc206 100644 --- a/dag/gunbc/live_deploy/member_observe.dag +++ b/dag/gunbc/live_deploy/member_observe.dag @@ -1,6 +1,7 @@ module gunbc.live_deploy.member_observe import std.types { String, Bool, List, Int, NonEmptyStr, CommitSha, FilePath } +import std.optional { Optional, Present, Absent } import std.algebra { trim } import std.content_hash { content_hash_atom } import extdeps.crypto.hash { Digest, sha256sum_argv, sha256sum_line_digest } @@ -159,20 +160,26 @@ fn entry_presence_in_parent(arm: ObservationArm, parent: String, name: String) - } fn entry_presence(arm: ObservationArm, path: String) -> EntryPresence { + entry_presence_above(arm: arm, path: path, below: Absent) +} + +// The ancestor walk in tail position, so the Rust realization lowers it to a loop. `below` is the +// cause read one level down: an ancestor that is present answers with that cause, an absent one +// answers absent, and the root answers with its own cause -- the same answers the nested walk gave. +fn entry_presence_above(arm: ObservationArm, path: String, below: Optional) -> EntryPresence { let parent = path_parent(path: path) let name = path_basename(path: path) match entry_presence_in_parent(arm: arm, parent: parent, name: name) { - EntryPresent => EntryPresent + EntryPresent => match below { + Absent => EntryPresent + Present { value: cause } => EntryPresenceIndeterminate { cause: cause } + } EntryAbsent => EntryAbsent EntryPresenceIndeterminate { cause } => if parent == "/" { EntryPresenceIndeterminate { cause: cause } } else { - match entry_presence(arm: arm, path: parent) { - EntryAbsent => EntryAbsent - EntryPresent => EntryPresenceIndeterminate { cause: cause } - EntryPresenceIndeterminate { cause: ancestor_cause } => EntryPresenceIndeterminate { cause: ancestor_cause } - } + entry_presence_above(arm: arm, path: parent, below: Present { value: cause }) } } } diff --git a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag index 32513bd0fb5..b166a01a86b 100644 --- a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag +++ b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag @@ -5,6 +5,7 @@ import gunbc.runner_microvm_network_observe { observe_slot_network_readings } import extdeps.virtualization.firecracker { FirecrackerVmConfig, firecracker_vm_config_document } import std.types { String, NonEmptyStr, Bool, List, Int } +import std.optional { Present, Absent } import std.nat { Nat } import std.checked_arithmetic { checked_int_magnitude, CheckedNatReady, CheckedNatOverflow } import extdeps.clock { clock_unix_millis_read, epoch_secs_of_millis, ClockUnixMillisObserved, ClockUnixMillisRefused } @@ -162,6 +163,8 @@ type CgroupWalkNode { path: NonEmptyStr, pids: List } type CgroupWalk { nodes: List } +type CgroupWalkPending { path: NonEmptyStr, depth: Int } + fn cgroup_procs_path(cgroup_path: NonEmptyStr) -> NonEmptyStr { cgroup_v2_child_path(parent: cgroup_path, child: cgroup_v2_procs_interface_file) } @@ -192,17 +195,26 @@ fn cgroup_child_names(cgroup_path: NonEmptyStr) -> List fn walk_cgroup(cgroup_path: NonEmptyStr, depth: Int) -> CgroupWalk { - let here = CgroupWalkNode { path: cgroup_path, pids: cgroup_procs_pids(cgroup_path: cgroup_path) } - if depth <= 0 { - CgroupWalk { nodes: [here] } - } else { - fold(cgroup_child_names(cgroup_path: cgroup_path), init: CgroupWalk { nodes: [here] }, f: (acc, name) => - CgroupWalk { - nodes: concat( - acc.nodes, - walk_cgroup(cgroup_path: cgroup_v2_child_path(parent: cgroup_path, child: trim(s: name) as NonEmptyStr), depth: depth - 1).nodes, - ), - }) + walk_cgroup_pending(pending: [CgroupWalkPending { path: cgroup_path, depth: depth }], nodes: []) +} + +// The subtree walk in tail position, so the Rust realization lowers it to a loop. The pending +// list is the unvisited frontier in preorder: a node's children go ahead of its later siblings, +// so nodes come out in the order the nested walk produced, and each cgroup is read the same way. +fn walk_cgroup_pending(pending: List, nodes: List) -> CgroupWalk +{ + match pending.first() { + Absent => CgroupWalk { nodes: nodes } + Present { value: next } => { + let here = CgroupWalkNode { path: next.path, pids: cgroup_procs_pids(cgroup_path: next.path) } + let children = if next.depth <= 0 { + [] + } else { + map(cgroup_child_names(cgroup_path: next.path), name => + CgroupWalkPending { path: cgroup_v2_child_path(parent: next.path, child: trim(s: name) as NonEmptyStr), depth: next.depth - 1 }) + } + walk_cgroup_pending(pending: concat(children, pending.skip(n: 1)), nodes: concat(nodes, [here])) + } } } From 181dd1b91bae8bc60725b2dec0db8ddc387f4aac Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 9 Oct 2026 18:20:38 +0000 Subject: [PATCH 27/27] integration/silent-lark-156: regenerate generated artifacts after merging #13472 and #13610 Produced by main_wet + claim_executor --required-regen on BuildBuddy at fd4421d6; second regen round installed nothing (fixed point). Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/design-rung-drops.md | 6 +- src/v1/stage0/src/v1_compiler_emit_rust.rs | 299 +++++++++++++++------ 2 files changed, 214 insertions(+), 91 deletions(-) diff --git a/docs/design-rung-drops.md b/docs/design-rung-drops.md index 49916065be5..5ccb89556e9 100644 --- a/docs/design-rung-drops.md +++ b/docs/design-rung-drops.md @@ -142,9 +142,11 @@ an append to the fabric event log by a principal that is not a fleet writer: RUN per-module rust emit refusals of corpus modules reachable from required-floor fixture closures, outside the v1 seed closure: RUNG DROP, mechanically preventable -> mitigatable (lost as a passenger of gunbc#13037 (floor C1): compile_dag_rust_emit_check and compile_dag_diagnostic_census stopped rendering every closure module (RenderEveryModule -> v1_compiler.cli_run fixture_render_selection)). Population: every corpus module in the transitive import closure of a fixture source passed to compile_dag_rust_emit_check or compile_dag_diagnostic_census on the required floor, that is not a v1 seed module (no `// Source module:` header under src/v1/stage0/src) or is a seed mirror that the affected-set bound (v1_compiler.required_regen_host scope_selection) does not select on the run in question. Restored when: every corpus module reachable from a required-floor fixture closure is rendered at least once per required run through the rust emitter, and a per-module emit refusal refuses the floor. -### fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize — declared 2026-10-06 +### fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize — declared 2026-10-06 · RETIRED -fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the rust emitted realization implementing the declared WitnessStderrCapturePolicy, the same next-rung trigger v1.compiler.emit shell_channel_realized_by_target already names). Population: exactly gunbc.stderr_capture_gap_exclusion.exclusion (consumed here as capture_gap_exclusion_population): gunbc.WitnessBin.Run in extdeps.gunbc. Output channels stderr_truncated, stderr_total_bytes, stderr_retained_bytes, each a TransportEmissionNotModeled ShellChannelNotRealizedByTarget for target rust. A later shell operation with the same capture-gap fact is NOT a member: the union still renders it and still refuses. The union compiles and evaluates that record; deleting either this module or the carrier refuses the seed compile. Restored when: THE CAPABILITY: the rust emitted shell realization implements the operation's declared WitnessStderrCapturePolicy, sufficient for v1.compiler.emit shell_channel_realized_by_target to return true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes. WHAT THAT MUST BE SUFFICIENT FOR: those operations emit for rust without TransportEmissionNotModeled, this exclusion has an empty population and this row and the union's strip of those services delete together. Answering the channels as untruncated whole-stream lengths does NOT discharge it. Deleting the channels from the interface does NOT discharge it. Silently omitting the module from the union without naming this cause does NOT discharge it. +**RETIRED — TRIGGER FIRED.** 2026-10-08 -- gunbc#13472. THE CAPABILITY, EXECUTED: v1.compiler.emit shell_channel_realized_by_target returns true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes on rust because emit_shell_call implements the declared WitnessStderrCapturePolicy. WitnessBin.Run emits without TransportEmissionNotModeled; the union strip of that service is deleted; this exclusion population is empty. DISCRIMINATING CONTROL, ENROLLED: v1_compiler.cli_run fixture_closure_union_controls admits a fixture whose closure reaches extdeps.gunbc, and a real emit error beside that reach still refuses as FixtureClosureUnionEmitRefused at module=efr_member. Answering the channels as untruncated whole-stream lengths did not discharge this. Deleting the channels from the interface did not discharge this. + +fixture-closure union rust emit of a shell operation whose modeled capture-accounting channels the rust target cannot realize: RUNG DROP, mechanically preventable -> mitigatable (replacement staged: the rust emitted realization implementing the declared WitnessStderrCapturePolicy, the same next-rung trigger v1.compiler.emit shell_channel_realized_by_target already names). Population: . Restored when: THE CAPABILITY: the rust emitted shell realization implements the operation's declared WitnessStderrCapturePolicy, sufficient for v1.compiler.emit shell_channel_realized_by_target to return true for ShellChanStderrTruncated, ShellChanStderrTotalBytes and ShellChanStderrRetainedBytes. WHAT THAT MUST BE SUFFICIENT FOR: those operations emit for rust without TransportEmissionNotModeled, this exclusion has an empty population and this row and the union's strip of those services delete together. Answering the channels as untruncated whole-stream lengths does NOT discharge it. Deleting the channels from the interface does NOT discharge it. Silently omitting the module from the union without naming this cause does NOT discharge it. ### new-witness eval-step cost gate over the one claim that runs the real fleet-converge build-job step list to hold the reset-observer dispatch admission's membership: it still executes, eval_steps stay recorded, a semantic red still blocks; only the eval-step cost-gate rung is lowered — declared 2026-09-30 diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index e28b0327929..56bc25a272d 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -178,7 +178,9 @@ use crate::v1_compiler_emit::FileVerb::{ FileDelete, FileLinkCreateNew, FileList, FileRead, FileWrite, FileWriteCreateNew, FileWriteCreateNewWithMode, FileWriteOwnerOnly, }; -use crate::v1_compiler_emit::ShellEmissionRefusal::ShellChannelNotRealizedByTarget; +use crate::v1_compiler_emit::ShellEmissionRefusal::{ + ShellCapturePolicyInputAbsent, ShellChannelNotRealizedByTarget, +}; use crate::v1_compiler_emit::ShellResultChannel::{ ShellChanExitCode, ShellChanExitSuccess, ShellChanStderr, ShellChanStderrRetainedBytes, ShellChanStderrTotalBytes, ShellChanStderrTruncated, ShellChanStdout, ShellChanStdoutLines, @@ -193,10 +195,11 @@ pub use crate::v1_compiler_emit::{ emit_typed_if_shared, emit_typed_let_shared, emit_unary_op, escape_rust_interp_text, extract_modifier_names, has_nested_records_node, has_service_items, is_null_coalesce, is_self_recursive, is_tco_eligible, keyed_container_has_target_inhabitant, - lookup_item_by_identity, module_emit_scope, order_typed_call_args_from_semantics, - render_node_type, render_tuple_parts, rust_literal_for_pattern, scope_after_expr, - seed_bindings, service_fallback_transport, service_field_ctors, service_field_decls, - shared_tco_reassign, shell_emission_refusal_fact, shell_result_channel_key, + lookup_item_by_identity, module_emit_scope, operation_declares_required_stderr_capture_policy, + order_typed_call_args_from_semantics, render_node_type, render_tuple_parts, + rust_literal_for_pattern, scope_after_expr, seed_bindings, service_fallback_transport, + service_field_ctors, service_field_decls, shared_tco_reassign, + shell_channel_is_capture_accounting, shell_emission_refusal_fact, shell_result_channel_key, tco_loop_iteration_lets, tco_loop_slot_name, tco_reassign_core, transport_binding_refusal_fact, }; pub use crate::v1_compiler_emit::{ @@ -37468,6 +37471,7 @@ pub fn emit_transport_call( rsf.clone(), op_node.clone(), source_indices.clone(), + env.clone(), ), BoundOperation::FileBound { verb: v, @@ -38823,7 +38827,7 @@ pub fn emit_exit_code_handling( v1_rt::concat( v1_rt::concat( "\n _ => { ".to_string(), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38885,7 +38889,7 @@ pub fn emit_exit_arm( v1_rt::concat(" ".to_string(), pattern.clone()), " => { ".to_string(), ), - shell_stderr_binding_line(), + shell_error_stderr_binding(result_fields.clone()), ), " ".to_string(), ), @@ -38905,6 +38909,7 @@ pub fn emit_shell_call( result_fields: Rc>>, op_node: Rc, source_indices: Rc>>, + env: Rc, ) -> String { { let argv = transport.children.clone(); @@ -38959,7 +38964,8 @@ pub fn emit_shell_call( Some(_) => true, std::option::Option::None => false, }; - let let_kw = if has_stdin.clone() { + let needs_capture = shell_needs_capture_accounting(result_fields.clone()); + let let_kw = if (has_stdin.clone() || needs_capture.clone()) { "let mut output".to_string() } else { "let output".to_string() @@ -39078,79 +39084,136 @@ pub fn emit_shell_call( __result }); let wd_line = " .current_dir(self.working_dir.as_deref().unwrap_or(\".\"))".to_string(); - if has_stdin.clone() { + let return_line = emit_exit_code_handling( + op_node.clone(), + result_fields.clone(), + source_indices.clone(), + ); + if needs_capture.clone() { { - let stdin_expr = - crate::v1_std_core::transport_stdin(transport.clone(), source_indices.clone()) + let policy_bind = emit_shell_stderr_policy_binding(op_node.clone(), env.clone()); + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let stdin_thread = if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) .clone() .unwrap(); - let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { - ExprData::ExprVar { - binding_kind: _, .. - } => crate::v1_compiler_emit::emit_ident( - crate::v1_std_core::expr_var_name_at( - stdin_expr.clone(), - source_indices.clone(), - ), - RenderTarget::Rust, - ), - _ => crate::v1_compiler_emit::emit_simple_expr( - stdin_expr.clone(), - RenderTarget::Rust, - source_indices.clone(), - ), + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + v1_rt::concat(v1_rt::concat("let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n use std::io::Write;\n if let Some(mut stdin) = stdin_pipe {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n Ok(())\n});\n".to_string()) + } + } else { + "let mut stdin_pipe = output.stdin.take();\nlet __stdin_thread = std::thread::spawn(move || -> std::io::Result<()> {\n drop(stdin_pipe);\n Ok(())\n});\n".to_string() }; - let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); - let spawn_exec = " .spawn()?;".to_string(); - let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); - let wait_line = "let output = output.wait_with_output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), + let capture_lines = v1_rt::concat( + v1_rt::concat(shell_capture_drain_start(), stdin_thread.clone()), + shell_capture_join_project(), ); - let all_lines = v1_rt::concat( + v1_rt::concat( v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + v1_rt::concat( + Rc::new(vec![policy_bind.clone(), cmd_line.clone()]), + arg_lines.clone(), + ), env_lines.clone(), ), Rc::new(vec![ wd_line.clone(), spawn_line.clone(), spawn_exec.clone(), - write_block.clone(), - wait_line.clone(), - check_line.clone(), + capture_lines.clone(), return_line.clone(), ]), - ); - all_lines.clone().join(&"\n".to_string()) + ) + .join(&"\n".to_string()) } } else { - { - let output_line = " .output()?;".to_string(); - let check_line = - "let stdout = String::from_utf8_lossy(&output.stdout).to_string();".to_string(); - let return_line = emit_exit_code_handling( - op_node.clone(), - result_fields.clone(), - source_indices.clone(), - ); - let all_lines = v1_rt::concat( + if has_stdin.clone() { + { + let stdin_expr = crate::v1_std_core::transport_stdin( + transport.clone(), + source_indices.clone(), + ) + .clone() + .unwrap(); + let stdin_var = match (*stdin_expr.expr_data.clone()).clone() { + ExprData::ExprVar { + binding_kind: _, .. + } => crate::v1_compiler_emit::emit_ident( + crate::v1_std_core::expr_var_name_at( + stdin_expr.clone(), + source_indices.clone(), + ), + RenderTarget::Rust, + ), + _ => crate::v1_compiler_emit::emit_simple_expr( + stdin_expr.clone(), + RenderTarget::Rust, + source_indices.clone(), + ), + }; + let spawn_line = " .stdin(std::process::Stdio::piped())\n .stdout(std::process::Stdio::piped())\n .stderr(std::process::Stdio::piped())".to_string(); + let spawn_exec = " .spawn()?;".to_string(); + let write_block = v1_rt::concat(v1_rt::concat("{\n use std::io::Write;\n if let Some(mut stdin) = output.stdin.take() {\n stdin.write_all(".to_string(), stdin_var.clone()), ".as_bytes())?;\n }\n}".to_string()); + let wait_line = "let output = output.wait_with_output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); v1_rt::concat( - v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), - env_lines.clone(), - ), - Rc::new(vec![ - wd_line.clone(), - output_line.clone(), - check_line.clone(), - return_line.clone(), - ]), - ); - all_lines.clone().join(&"\n".to_string()) + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + spawn_line.clone(), + spawn_exec.clone(), + write_block.clone(), + wait_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } + } else { + { + let output_line = " .output()?;".to_string(); + let check_line = + "let stdout = String::from_utf8_lossy(&output.stdout).to_string();" + .to_string(); + v1_rt::concat( + v1_rt::concat( + v1_rt::concat(Rc::new(vec![cmd_line.clone()]), arg_lines.clone()), + env_lines.clone(), + ), + Rc::new(vec![ + wd_line.clone(), + output_line.clone(), + check_line.clone(), + return_line.clone(), + ]), + ) + .join(&"\n".to_string()) + } } } } @@ -39334,6 +39397,66 @@ pub fn shell_stderr_binding_line() -> String { CACHED.with(|c: &String| c.clone()) } +pub fn shell_needs_capture_accounting(result_fields: Rc>>) -> bool { + { + let mut __found = false; + for f in result_fields.iter().cloned() { + if crate::v1_compiler_emit::shell_channel_is_capture_accounting(f.channel.clone()) { + __found = true; + break; + } + } + __found + } +} + +pub fn shell_error_stderr_binding(result_fields: Rc>>) -> String { + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { + v1_rt::concat(shell_stderr_binding_line(), " ".to_string()) + } +} + +pub fn emit_shell_stderr_policy_binding(op_node: Rc, env: Rc) -> String { + if crate::v1_compiler_emit::operation_declares_required_stderr_capture_policy( + op_node.clone(), + env.clone(), + ) { + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("let (__stderr_complete_limit, __stderr_complete_source): (Option, String) = match &*stderr_capture {\n".to_string(), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => {\n".to_string()), " match v1_rt::read_host_budget_bytes() {\n".to_string()), " (Some(n), source) => (Some(n as usize), source),\n".to_string()), " (None, source) => return Err(format!(\"WitnessStderrCaptureCompleteBudgetUnreadable: Complete stderr capture requires the active host budget authority ({})\", source).into()),\n".to_string()), " }\n".to_string()), " }\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => {\n".to_string()), " let n = crate::std_measure::byte_size_count(bytes.clone());\n".to_string()), " if n < 0 { return Err(\"WitnessStderrCapturePolicy.BoundedTail bytes must be non-negative\".into()); }\n".to_string()), " (None, String::new())\n".to_string()), " }\n".to_string()), "};\n".to_string()), "let __stderr_tail_bytes: usize = match &*stderr_capture {\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::BoundedTail { bytes } => crate::std_measure::byte_size_count(bytes.clone()) as usize,\n".to_string()), " crate::std_shell_stream_capture::WitnessStderrCapturePolicy::Complete => 0,\n".to_string()), "};\n".to_string()) + } else { + v1_rt::concat( + v1_rt::concat( + "return Err(\"".to_string(), + crate::v1_compiler_emit::shell_emission_refusal_fact(Rc::new( + ShellEmissionRefusal::ShellCapturePolicyInputAbsent { + key: "stderr_truncated".to_string(), + }, + )), + ), + "\".into());\n".to_string(), + ) + } +} + +pub fn shell_capture_drain_start() -> String { + thread_local! { + static CACHED: String = { + "let mut stdout_pipe = output.stdout.take();\nlet mut stderr_pipe = output.stderr.take();\nlet stdout_thread = std::thread::spawn(move || -> std::io::Result> {\n let mut buf = Vec::new();\n if let Some(ref mut reader) = stdout_pipe {\n std::io::Read::read_to_end(reader, &mut buf)?;\n }\n Ok(buf)\n});\nlet stderr_thread = std::thread::spawn(move || -> std::io::Result<(Vec, u64, bool)> {\n let mut reader = match stderr_pipe {\n Some(r) => r,\n None => return Ok((Vec::new(), 0, false)),\n };\n let mut chunk = [0u8; 65536];\n let mut total: u64 = 0;\n if let Some(max_bytes) = __stderr_complete_limit {\n let mut retained = Vec::new();\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n if total <= max_bytes as u64 { retained.extend_from_slice(&chunk[..n]); }\n }\n let truncated = total > max_bytes as u64;\n return Ok((if truncated { Vec::new() } else { retained }, total, truncated));\n }\n let cap = __stderr_tail_bytes;\n let mut ring = Vec::with_capacity(cap);\n let mut start = 0usize;\n loop {\n let n = std::io::Read::read(&mut reader, &mut chunk)?;\n if n == 0 { break; }\n total += n as u64;\n for &b in &chunk[..n] {\n if cap == 0 { continue; }\n if ring.len() < cap {\n ring.push(b);\n } else {\n ring[start] = b;\n start = (start + 1) % cap;\n }\n }\n }\n let retained = if ring.len() < cap || cap == 0 {\n ring\n } else {\n let mut out = Vec::with_capacity(cap);\n for i in 0..cap { out.push(ring[(start + i) % cap]); }\n out\n };\n let retained_len = retained.len() as u64;\n Ok((retained, total, total > retained_len))\n});\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + +pub fn shell_capture_join_project() -> String { + thread_local! { + static CACHED: String = { + "let status = output.wait()?;\n__stdin_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdin write thread panicked\"))??;\nlet stdout_bytes = stdout_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stdout drain thread panicked\"))??;\nlet (stderr_bytes, stderr_total_u64, stderr_truncated) = stderr_thread.join().map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, \"stderr drain thread panicked\"))??;\nif let Some(max_bytes) = __stderr_complete_limit {\n if stderr_total_u64 > max_bytes as u64 {\n return Err(format!(\"WitnessStderrCaptureCompleteBudgetExceeded: stderr total {} exceeds host budget {} ({})\", stderr_total_u64, max_bytes, __stderr_complete_source).into());\n }\n}\nlet stderr_total_bytes = stderr_total_u64 as i64;\nlet stderr_retained_bytes = stderr_bytes.len() as i64;\nlet stdout = String::from_utf8_lossy(&stdout_bytes).to_string();\nlet stderr = String::from_utf8_lossy(&stderr_bytes).trim_end().to_string();\nlet output = std::process::Output { status, stdout: stdout_bytes, stderr: stderr_bytes };\n".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn emit_shell_return(result_fields: Rc>>) -> String { v1_rt::concat( v1_rt::concat( @@ -39383,24 +39506,28 @@ pub fn shell_projection_value(result_fields: Rc>>) -> S } pub fn shell_stderr_prelude(result_fields: Rc>>) -> String { - { - let needs_stderr = { - let mut __found = false; - for f in result_fields.iter().cloned() { - if match f.channel.clone() { - ShellResultChannel::ShellChanStderr => true, - _ => false, - } { - __found = true; - break; + if shell_needs_capture_accounting(result_fields.clone()) { + "".to_string() + } else { + { + let needs_stderr = { + let mut __found = false; + for f in result_fields.iter().cloned() { + if match f.channel.clone() { + ShellResultChannel::ShellChanStderr => true, + _ => false, + } { + __found = true; + break; + } } + __found + }; + if needs_stderr.clone() { + v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) + } else { + "".to_string() } - __found - }; - if needs_stderr.clone() { - v1_rt::concat(shell_stderr_binding_line(), "\n".to_string()) - } else { - "".to_string() } } } @@ -39418,15 +39545,9 @@ pub fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: bool) - "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()" .to_string(), ), - ShellResultChannel::ShellChanStderrTruncated => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrTotalBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } - ShellResultChannel::ShellChanStderrRetainedBytes => { - emit_unrealizable_shell_channel(channel.clone()) - } + ShellResultChannel::ShellChanStderrTruncated => "stderr_truncated".to_string(), + ShellResultChannel::ShellChanStderrTotalBytes => "stderr_total_bytes".to_string(), + ShellResultChannel::ShellChanStderrRetainedBytes => "stderr_retained_bytes".to_string(), }; if is_optional.clone() { v1_rt::concat(