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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion dag/gunbc/interpreter_kernel_model.dag
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type V1InterpreterDissolutionPhase
| PureEvalEmitWired
| HostPhysicsPinnedTerminal

data interpreter_kernel_d_current_phase: V1InterpreterDissolutionPhase = ModelAndWitnessOnly
data interpreter_kernel_d_current_phase: V1InterpreterDissolutionPhase = PureEvalEmitWired

data pure_eval_authority_modules: List<String> = [
"src/v2/compiler/05_eval.dag",
Expand Down Expand Up @@ -56,6 +56,14 @@ fn interpreter_kernel_d_phase_is_model_only() -> Bool {
}
}

fn interpreter_kernel_d_phase_is_pure_eval_emit_wired() -> Bool {
match interpreter_kernel_d_current_phase {
PureEvalEmitWired => true
ModelAndWitnessOnly => false
HostPhysicsPinnedTerminal => false
}
}

fn v1_interpreter_not_classified_as_pinned_host_physics() -> Bool {
!is_pinned_host_physics_stage0_file(basename: v1_interpreter_hybrid_seed_file)
}
Expand Down
8 changes: 4 additions & 4 deletions dag/test/claim/interpreter_kernel_model_witness_test.dag
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import gunbc.interpreter_kernel_model {
is_pinned_host_physics_stage0_file,
pinned_host_physics_file_count,
pure_eval_authority_module_count,
interpreter_kernel_d_phase_is_model_only,
interpreter_kernel_d_phase_is_pure_eval_emit_wired,
v1_interpreter_not_classified_as_pinned_host_physics,
seam_class_for_authority_module,
seam_class_for_pinned_basename,
Expand Down Expand Up @@ -40,8 +40,8 @@ fn w_hybrid_interpreter_file_boundary() -> Bool {
v1_interpreter_not_classified_as_pinned_host_physics()
}

fn w_kernel_d_phase_model_only() -> Bool {
interpreter_kernel_d_phase_is_model_only()
fn w_kernel_d_phase_pure_eval_emit_wired() -> Bool {
interpreter_kernel_d_phase_is_pure_eval_emit_wired()
}

fn w_seam_class_authority_modules() -> Bool {
Expand Down Expand Up @@ -71,7 +71,7 @@ test fn interpreter_kernel_model_witnesses() -> Bool {
w_pure_eval_authority_modules() &&
w_pinned_host_physics_roster() &&
w_hybrid_interpreter_file_boundary() &&
w_kernel_d_phase_model_only() &&
w_kernel_d_phase_pure_eval_emit_wired() &&
w_seam_class_authority_modules() &&
w_hybrid_seed_seam_unclassified()
}
1 change: 1 addition & 0 deletions src/v1/04_method.dag
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ fn builtin_function_registry() -> Map<String, Node> {
let m = map_insert(m, "starts_with", bool_type)
let m = map_insert(m, "replace", string_type)
let m = map_insert(m, "filesystem_read", type_variable_node(id: "filesystem_read_result"))
let m = map_insert(m, "emit_host_run_transport", type_variable_node(id: "emit_host_run_transport_result"))
let m = map_insert(m, "contiguous_loop_elementwise_kernel", list_of_type_variable(id: "collection_element"))
let m = map_insert(m, "contiguous_loop_elementwise_float_kernel", list_of_type_variable(id: "collection_element"))
let m = map_insert(m, "shell_materialize_argv_for_operation", list_of_element(element: string_type))
Expand Down
38 changes: 26 additions & 12 deletions src/v1/stage0/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ enum Commands {
target: String,
#[arg(long = "dependency-pool-index", default_value = "strict")]
dependency_pool_index: String,
/// Entry `.dag` file: compile only this module and its transitive imports
/// (not every `.dag` file under the first --source-root). Scopes the compile
/// to a subtree so a small closure can be emitted without a whole-tree pass.
#[arg(long)]
entry: Option<String>,
},
Ci,

Expand Down Expand Up @@ -307,6 +312,7 @@ fn main() {
output_dir,
target,
dependency_pool_index,
entry,
} => {
let render_targets = parse_render_targets(&target);
let pool_index = parse_dependency_pool_index(&dependency_pool_index);
Expand All @@ -319,19 +325,27 @@ fn main() {
source_roots.len()
);

// Entry modules: all .dag files in the FIRST source root.
// Additional roots are dependency pools resolved via imports.
// This is intentional: --source-root src/v1 --source-root dag
// means 'compile src/v1, using dag as a dependency pool.'
let first_root = std::path::Path::new(&source_roots[0]);
// Entry modules. With --entry: exactly the one named file (its
// transitive imports are resolved from the index below), so a small
// subtree compiles without a whole-tree pass. Without --entry: all
// .dag files in the FIRST source root (additional roots are dependency
// pools resolved via imports — `--source-root src/v1 --source-root dag`
// means 'compile src/v1, using dag as a dependency pool').
let mut entry_files = Vec::new();
if first_root.is_dir() {
let mut dag_paths = Vec::new();
collect_dag_files(first_root, &mut dag_paths);
for path in dag_paths {
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e));
entry_files.push((path.to_string_lossy().to_string(), content));
if let Some(entry_path) = &entry {
let content = std::fs::read_to_string(entry_path)
.unwrap_or_else(|e| panic!("failed to read entry {:?}: {}", entry_path, e));
entry_files.push((entry_path.clone(), content));
} else {
let first_root = std::path::Path::new(&source_roots[0]);
if first_root.is_dir() {
let mut dag_paths = Vec::new();
collect_dag_files(first_root, &mut dag_paths);
for path in dag_paths {
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {:?}: {}", path, e));
entry_files.push((path.to_string_lossy().to_string(), content));
}
}
}
let skipped_moduleless = cli_run::moduleless_dag_entry_paths(&entry_files);
Expand Down
5 changes: 5 additions & 0 deletions src/v1/stage0/src/v1_compiler_infer_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ pub fn builtin_function_registry() -> Rc<HashMap<String, Rc<Node>>> {
"filesystem_read".to_string(),
type_variable_node("filesystem_read_result".to_string()),
);
let m = v1_rt::rc_map_insert(
m.clone(),
"emit_host_run_transport".to_string(),
type_variable_node("emit_host_run_transport_result".to_string()),
);
let m = v1_rt::rc_map_insert(
m.clone(),
"contiguous_loop_elementwise_kernel".to_string(),
Expand Down
252 changes: 252 additions & 0 deletions src/v1/stage0/src/v1_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5182,6 +5182,251 @@ fn eval_filesystem_read_builtin(path: String, ctx: &InterpContext) -> InterpResu
})
}

/// Host tap for `v2.compiler.emit_host.run_host_process` (kernel-D emit_host transport):
/// materialize a workspace from resolved `{path, text}` rows, run the build argvs then the
/// run argv with typed argv (no shell), and return exit/stdout/stderr/build-log as data.
/// Wet-mode only — hermetic execution refuses instead of mocking (no fabricated receipt).
fn eval_emit_host_run_transport_builtin(
files_arg: Option<&Value>,
build_arg: Option<&Value>,
run_arg: Option<&Value>,
ctx: &InterpContext,
) -> InterpResult<Value> {
if ctx.execution_mode.is_hermetic() {
return Err(InterpError::TypeError {
msg: "hermetic mode: emit_host_run_transport refuses host process execution \
(no mock arm; run wet or record a fixture)"
.to_string(),
});
}

let files_val = files_arg.ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport requires (files, build, run) arguments".to_string(),
})?;
let files = free_monoid_to_vec(files_val).ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: files must be a List of {path, text} records".to_string(),
})?;
let mut workspace_files: Vec<(String, String)> = Vec::with_capacity(files.len());
for f in &files {
match f {
Value::Record { fields, .. } => {
let path = ctx
.field(fields, "path")
.and_then(free_monoid_to_string)
.ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: workspace file missing String path"
.to_string(),
})?;
let text = ctx
.field(fields, "text")
.and_then(free_monoid_to_string)
.ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: workspace file missing String text"
.to_string(),
})?;
workspace_files.push((path, text));
}
other => {
return Err(InterpError::TypeError {
msg: format!(
"emit_host_run_transport: workspace entry must be a record, got {}",
other.type_label()
),
})
}
}
}

let build_val = build_arg.ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport requires (files, build, run) arguments".to_string(),
})?;
let build_lists = free_monoid_to_vec(build_val).ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: build must be a List of argv Lists".to_string(),
})?;
let mut build_argvs: Vec<Vec<String>> = Vec::with_capacity(build_lists.len());
for argv_val in &build_lists {
let argv_items = free_monoid_to_vec(argv_val).ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: build argv must be a List<String>".to_string(),
})?;
let mut argv: Vec<String> = Vec::with_capacity(argv_items.len());
for item in &argv_items {
let s = free_monoid_to_string(item).ok_or_else(|| InterpError::TypeError {
msg: format!(
"emit_host_run_transport: build argv element must be String, got {}",
item.type_label()
),
})?;
argv.push(s);
}
if argv.is_empty() {
return Err(InterpError::TypeError {
msg: "emit_host_run_transport: empty build argv".to_string(),
});
}
build_argvs.push(argv);
}

let run_arg_items =
run_arg
.and_then(free_monoid_to_vec)
.ok_or_else(|| InterpError::TypeError {
msg: "emit_host_run_transport: run must be a List<String>".to_string(),
})?;
let mut run_argv: Vec<String> = Vec::with_capacity(run_arg_items.len());
for item in &run_arg_items {
let s = free_monoid_to_string(item).ok_or_else(|| InterpError::TypeError {
msg: format!(
"emit_host_run_transport: run argv element must be String, got {}",
item.type_label()
),
})?;
run_argv.push(s);
}
if run_argv.is_empty() {
return Err(InterpError::TypeError {
msg: "emit_host_run_transport: empty run argv".to_string(),
});
}

use std::sync::atomic::{AtomicU64, Ordering};
static EMIT_HOST_WORKSPACE_COUNTER: AtomicU64 = AtomicU64::new(0);
let workspace = std::env::temp_dir().join(format!(
"gunbc-emit-host-{}-{}",
std::process::id(),
EMIT_HOST_WORKSPACE_COUNTER.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&workspace).map_err(|e| InterpError::TypeError {
msg: format!("emit_host_run_transport: workspace create failed: {e}"),
})?;

let result = emit_host_run_transport_in_workspace(
&workspace,
&workspace_files,
&build_argvs,
&run_argv,
ctx,
);
if let Err(cleanup) = std::fs::remove_dir_all(&workspace) {
eprintln!(
"emit_host_run_transport: workspace cleanup failed ({}): {cleanup}",
workspace.display()
);
}
result
}

fn emit_host_run_transport_in_workspace(
workspace: &std::path::Path,
files: &[(String, String)],
build_argvs: &[Vec<String>],
run_argv: &[String],
ctx: &InterpContext,
) -> InterpResult<Value> {
use std::path::Component;

for (rel, text) in files {
let p = std::path::Path::new(rel);
if p.is_absolute() || p.components().any(|c| matches!(c, Component::ParentDir)) {
return Err(InterpError::TypeError {
msg: format!("emit_host_run_transport: workspace path escapes workspace: {rel}"),
});
}
let full = workspace.join(p);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).map_err(|e| InterpError::TypeError {
msg: format!(
"emit_host_run_transport: mkdir {} failed: {e}",
parent.display()
),
})?;
}
std::fs::write(&full, text).map_err(|e| InterpError::TypeError {
msg: format!(
"emit_host_run_transport: write {} failed: {e}",
full.display()
),
})?;
}

let target_dir = workspace.join("target");
let run_command = |argv: &[String]| -> InterpResult<std::process::Output> {
std::process::Command::new(&argv[0])
.args(&argv[1..])
.current_dir(workspace)
.env("CARGO_TARGET_DIR", &target_dir)
.output()
.map_err(|e| InterpError::TypeError {
msg: format!("emit_host_run_transport: spawn {:?} failed: {e}", argv[0]),
})
};

let transport_result = |phase: &str,
success: bool,
exit_code: i64,
stdout: &[u8],
stderr: &[u8],
build_log: Vec<Value>|
-> Value {
Value::Record {
type_name: ctx.sym("EmitHostTransportResult"),
fields: Rc::new(vec![
(ctx.sym("phase"), Value::Str(phase.to_string())),
(ctx.sym("success"), Value::Bool(success)),
(ctx.sym("exit_code"), Value::Int(exit_code)),
(
ctx.sym("stdout_octets"),
list_value(
stdout
.iter()
.map(|b| Value::Int(*b as i64))
.collect::<Vec<Value>>(),
),
),
(
ctx.sym("stderr_octets"),
list_value(
stderr
.iter()
.map(|b| Value::Int(*b as i64))
.collect::<Vec<Value>>(),
),
),
(ctx.sym("build_log"), list_value(build_log)),
]),
}
};

let mut build_log: Vec<Value> = Vec::new();
for argv in build_argvs {
let out = run_command(argv)?;
let code = out.status.code().map(i64::from).unwrap_or(-1);
build_log.push(Value::Str(format!("{} -> exit {code}", argv.join(" "))));
if !out.status.success() {
build_log.push(Value::Str(String::from_utf8_lossy(&out.stderr).to_string()));
return Ok(transport_result(
"build",
false,
code,
&out.stdout,
&out.stderr,
build_log,
));
}
}

let out = run_command(run_argv)?;
let code = out.status.code().map(i64::from).unwrap_or(-1);
build_log.push(Value::Str(format!("{} -> exit {code}", run_argv.join(" "))));
Ok(transport_result(
"run",
out.status.success(),
code,
&out.stdout,
&out.stderr,
build_log,
))
}

fn eval_builtin(
name: &str,
args: &[(Option<String>, Value)],
Expand Down Expand Up @@ -5633,6 +5878,13 @@ fn eval_builtin(
Ok(Some(eval_filesystem_read_builtin(path, ctx)?))
}

"emit_host_run_transport" => Ok(Some(eval_emit_host_run_transport_builtin(
positional.first().copied(),
positional.get(1).copied(),
positional.get(2).copied(),
ctx,
)?)),

"contiguous_loop_elementwise_kernel" => {
let op_codes = expect_int_list_flex(positional.first().copied(), name)?;
let a = expect_int_list_flex(positional.get(1).copied(), name)?;
Expand Down
Loading