feat(phase32): L-2.1 Stage-1 UsingResolver JoinIR integration + cleanup

Phase 32 L-2.1 complete implementation:

1. Stage-1 UsingResolver main line JoinIR connection
   - CFG-based LoopForm construction for resolve_for_source/5
   - LoopToJoinLowerer integration with handwritten fallback
   - JSON snapshot tests 6/6 PASS

2. JoinIR/VM Bridge improvements
   - Simplified join_ir_vm_bridge.rs dispatch logic
   - Enhanced json.rs serialization
   - PHI core boxes cleanup (local_scope_inspector, loop_exit_liveness, loop_var_classifier)

3. Stage-1 CLI enhancements
   - Extended args.rs, groups.rs, mod.rs for new options
   - Improved stage1_bridge module (args, env, mod)
   - Updated stage1_cli.hako

4. MIR builder cleanup
   - Simplified if_form.rs control flow
   - Removed dead code from loop_builder.rs
   - Enhanced phi_merge.rs

5. Runner module updates
   - json_v0_bridge/lowering.rs improvements
   - dispatch.rs, selfhost.rs, modes/vm.rs cleanup

6. Documentation updates
   - CURRENT_TASK.md, AGENTS.md
   - Various docs/ updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-26 10:17:37 +09:00
parent 7d0581c9a4
commit 51ff558904
36 changed files with 474 additions and 361 deletions

View File

@ -316,6 +316,25 @@ impl MirInterpreter {
Ok(ret.to_nyash_box())
}
/// Execute a specific function with explicit arguments (bypasses entry discovery).
pub fn execute_function_with_args(
&mut self,
module: &MirModule,
func_name: &str,
args: &[VMValue],
) -> Result<VMValue, VMError> {
// Snapshot functions for call resolution
self.functions = module.functions.clone();
let func = self
.functions
.get(func_name)
.ok_or_else(|| VMError::InvalidInstruction(format!("function not found: {}", func_name)))?
.clone();
self.exec_function_inner(&func, Some(args))
}
fn execute_function(&mut self, func: &MirFunction) -> Result<VMValue, VMError> {
self.exec_function_inner(func, None)
}

View File

@ -49,6 +49,11 @@ pub fn build_command() -> Command {
.arg(Arg::new("json-file").long("json-file").value_name("FILE").help("Read Ny JSON IR v0 from a file and execute via MIR Interpreter"))
.arg(Arg::new("mir-json-file").long("mir-json-file").value_name("FILE").help("[Diagnostic] Read MIR JSON v0 from a file and perform minimal validation/inspection (experimental)") )
.arg(Arg::new("emit-mir-json").long("emit-mir-json").value_name("FILE").help("Emit MIR JSON v0 to file and exit"))
.arg(Arg::new("emit-program-json").long("emit-program-json").value_name("FILE").help("Emit Program(JSON v0) to file and exit (AST direct; no Stage-1 stub)"))
.arg(Arg::new("hako-emit-program-json").long("hako-emit-program-json").value_name("FILE").help("Emit Program(JSON v0) via Stage-1 (.hako) stub and exit"))
.arg(Arg::new("hako-emit-mir-json").long("hako-emit-mir-json").value_name("FILE").help("Emit MIR(JSON) via Stage-1 (.hako) stub (json_v0_bridge path)"))
.arg(Arg::new("hako-run").long("hako-run").help("Run via Stage-1 (.hako) stub (equivalent to NYASH_USE_STAGE1_CLI=1)").action(clap::ArgAction::SetTrue))
.arg(Arg::new("emit-program-json").long("emit-program-json").value_name("FILE").help("Emit Program(JSON v0) to file and exit (AST direct)"))
.arg(Arg::new("program-json-to-mir").long("program-json-to-mir").value_name("FILE").help("Convert Program(JSON v0) to MIR(JSON) and exit (use with --json-file)"))
.arg(Arg::new("emit-exe").long("emit-exe").value_name("FILE").help("Emit native executable via ny-llvmc and exit"))
.arg(Arg::new("emit-exe-nyrt").long("emit-exe-nyrt").value_name("DIR").help("Directory containing libnyash_kernel.a (used with --emit-exe)"))
@ -121,6 +126,8 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
if let Some(a) = matches.get_one::<String>("ny-compiler-args") {
std::env::set_var("NYASH_NY_COMPILER_CHILD_ARGS", a);
}
let hako_emit_program_path = matches.get_one::<String>("hako-emit-program-json").cloned();
let hako_emit_mir_path = matches.get_one::<String>("hako-emit-mir-json").cloned();
let cfg = CliConfig {
file: matches.get_one::<String>("file").cloned(),
debug_fuel: parse_debug_fuel(matches.get_one::<String>("debug-fuel").unwrap()),
@ -182,7 +189,17 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
.get_many::<String>("using")
.map(|v| v.cloned().collect())
.unwrap_or_else(|| Vec::new()),
emit_mir_json: matches.get_one::<String>("emit-mir-json").cloned(),
emit_mir_json: matches
.get_one::<String>("emit-mir-json")
.cloned()
.or(hako_emit_mir_path.clone()),
emit_program_json: matches
.get_one::<String>("emit-program-json")
.cloned()
.or(hako_emit_program_path.clone()),
hako_emit_program_json: hako_emit_program_path.is_some(),
hako_emit_mir_json: hako_emit_mir_path.is_some(),
hako_run: matches.get_flag("hako-run"),
program_json_to_mir: matches.get_one::<String>("program-json-to-mir").cloned(),
emit_exe: matches.get_one::<String>("emit-exe").cloned(),
emit_exe_nyrt: matches.get_one::<String>("emit-exe-nyrt").cloned(),
@ -201,6 +218,35 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
if cfg.vm_stats_json {
std::env::set_var("NYASH_VM_STATS_JSON", "1");
}
// hako-prefixed Stage-1 stub routes
if cfg.hako_emit_program_json {
std::env::set_var("NYASH_USE_STAGE1_CLI", "1");
std::env::set_var("HAKO_STAGE1_MODE", "emit-program");
std::env::set_var("HAKO_EMIT_PROGRAM_JSON", "1");
std::env::set_var("STAGE1_EMIT_PROGRAM_JSON", "1");
if let Some(f) = cfg.file.as_ref() {
std::env::set_var("HAKO_STAGE1_INPUT", f);
std::env::set_var("NYASH_STAGE1_INPUT", f);
}
}
if cfg.hako_emit_mir_json {
std::env::set_var("NYASH_USE_STAGE1_CLI", "1");
std::env::set_var("HAKO_STAGE1_MODE", "emit-mir");
std::env::set_var("HAKO_EMIT_MIR_JSON", "1");
std::env::set_var("STAGE1_EMIT_MIR_JSON", "1");
if let Some(f) = cfg.file.as_ref() {
std::env::set_var("HAKO_STAGE1_INPUT", f);
std::env::set_var("NYASH_STAGE1_INPUT", f);
}
}
if cfg.hako_run {
std::env::set_var("NYASH_USE_STAGE1_CLI", "1");
std::env::set_var("HAKO_STAGE1_MODE", "run");
if let Some(f) = cfg.file.as_ref() {
std::env::set_var("HAKO_STAGE1_INPUT", f);
std::env::set_var("NYASH_STAGE1_INPUT", f);
}
}
if cfg.jit_exec {
std::env::set_var("NYASH_JIT_EXEC", "1");
}

View File

@ -57,6 +57,10 @@ pub struct BuildConfig {
pub struct EmitConfig {
pub emit_cfg: Option<String>,
pub emit_mir_json: Option<String>,
pub emit_program_json: Option<String>,
pub hako_emit_program_json: bool,
pub hako_emit_mir_json: bool,
pub hako_run: bool,
pub program_json_to_mir: Option<String>,
pub emit_exe: Option<String>,
pub emit_exe_nyrt: Option<String>,

View File

@ -43,6 +43,7 @@ pub struct CliConfig {
pub jit_only: bool,
pub jit_direct: bool,
pub emit_cfg: Option<String>,
pub emit_program_json: Option<String>,
pub cli_verbose: bool,
pub run_task: Option<String>,
pub load_ny_plugins: bool,
@ -59,6 +60,9 @@ pub struct CliConfig {
pub build_target: Option<String>,
pub cli_usings: Vec<String>,
pub emit_mir_json: Option<String>,
pub hako_emit_program_json: bool,
pub hako_emit_mir_json: bool,
pub hako_run: bool,
pub program_json_to_mir: Option<String>,
pub emit_exe: Option<String>,
pub emit_exe_nyrt: Option<String>,
@ -127,6 +131,10 @@ impl CliConfig {
emit: EmitConfig {
emit_cfg: self.emit_cfg.clone(),
emit_mir_json: self.emit_mir_json.clone(),
emit_program_json: self.emit_program_json.clone(),
hako_emit_program_json: self.hako_emit_program_json,
hako_emit_mir_json: self.hako_emit_mir_json,
hako_run: self.hako_run,
program_json_to_mir: self.program_json_to_mir.clone(),
emit_exe: self.emit_exe.clone(),
emit_exe_nyrt: self.emit_exe_nyrt.clone(),
@ -184,6 +192,7 @@ impl Default for CliConfig {
jit_native_f64: false,
jit_native_bool: false,
emit_cfg: None,
emit_program_json: None,
jit_only: false,
jit_direct: false,
cli_verbose: false,
@ -202,6 +211,9 @@ impl Default for CliConfig {
build_target: None,
cli_usings: Vec::new(),
emit_mir_json: None,
hako_emit_program_json: false,
hako_emit_mir_json: false,
hako_run: false,
program_json_to_mir: None,
emit_exe: None,
emit_exe_nyrt: None,

View File

@ -36,15 +36,37 @@ pub static OPERATORS_DIV_RULES: &[(&str, &str, &str, &str)] = &[
];
pub fn lookup_keyword(word: &str) -> Option<&'static str> {
for (k, t) in KEYWORDS {
if *k == word {
return Some(*t);
}
if *k == word { return Some(*t); }
}
None
}
pub static SYNTAX_ALLOWED_STATEMENTS: &[&str] = &[
"box", "global", "function", "static", "if", "loop", "break", "return", "print", "nowait",
"include", "local", "outbox", "try", "throw", "using", "from",
"box",
"global",
"function",
"static",
"if",
"loop",
"break",
"return",
"print",
"nowait",
"include",
"local",
"outbox",
"try",
"throw",
"using",
"from",
];
pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &["add", "sub", "mul", "div", "and", "or", "eq", "ne"];
pub static SYNTAX_ALLOWED_BINOPS: &[&str] = &[
"add",
"sub",
"mul",
"div",
"and",
"or",
"eq",
"ne",
];

View File

@ -1,5 +1,5 @@
use super::{MirBuilder, ValueId};
use crate::ast::{ASTNode, BinaryOperator};
use crate::ast::ASTNode;
use crate::mir::loop_api::LoopBuilderApi; // for current_block()
impl MirBuilder {
@ -13,35 +13,7 @@ impl MirBuilder {
) -> Result<ValueId, String> {
// Reserve a deterministic join id for debug region labeling
let join_id = self.debug_next_join_id();
// Heuristic pre-pin: if condition is a comparison, evaluate its operands now and pin them
// so that subsequent branches can safely reuse these values across blocks.
// This leverages existing variable_map merges (PHI) at the merge block.
if crate::config::env::mir_pre_pin_compare_operands() {
if let ASTNode::BinaryOp {
operator,
left,
right,
..
} = &condition
{
match operator {
BinaryOperator::Equal
| BinaryOperator::NotEqual
| BinaryOperator::Less
| BinaryOperator::LessEqual
| BinaryOperator::Greater
| BinaryOperator::GreaterEqual => {
if let Ok(lhs_v) = self.build_expression((**left).clone()) {
let _ = self.pin_to_slot(lhs_v, "@if_lhs");
}
if let Ok(rhs_v) = self.build_expression((**right).clone()) {
let _ = self.pin_to_slot(rhs_v, "@if_rhs");
}
}
_ => {}
}
}
}
// Pre-pin heuristic was deprecated; keep operands as-is for predictability.
let condition_val = self.build_expression(condition)?;
let condition_val = self.local_cond(condition_val);

View File

@ -110,6 +110,7 @@ impl<'a> PhiMergeHelper<'a> {
///
/// # Returns
/// Ok(()) on success, Err(String) on failure
#[allow(dead_code)] // Reserved: explicit dst PHI merge for future use
pub fn merge_with_dst(
&mut self,
dst: ValueId,

View File

@ -8,7 +8,7 @@
use std::io::Write;
use super::{
BinOpKind, CompareOp, ConstValue, JoinFunction, JoinInst, JoinModule, MirLikeInst, VarId,
BinOpKind, CompareOp, ConstValue, JoinFunction, JoinInst, JoinModule, MirLikeInst,
};
/// JoinModule を JSON としてシリアライズする

View File

@ -83,20 +83,6 @@ fn join_func_name(id: JoinFuncId) -> String {
format!("join_func_{}", id.0)
}
/// JoinValue → MirConstValue 変換
fn join_value_to_mir_const(value: &JoinValue) -> Result<MirConstValue, JoinIrVmBridgeError> {
match value {
JoinValue::Int(v) => Ok(MirConstValue::Integer(*v)),
JoinValue::Bool(b) => Ok(MirConstValue::Bool(*b)),
JoinValue::Str(s) => Ok(MirConstValue::String(s.clone())),
JoinValue::Unit => Ok(MirConstValue::Null),
_ => Err(JoinIrVmBridgeError::new(format!(
"Unsupported JoinValue type: {:?}",
value
))),
}
}
/// Phase 27-shortterm S-4.3: JoinIR → VM 実行のエントリーポイント
///
/// ## Arguments
@ -126,11 +112,11 @@ pub fn run_joinir_via_vm(
debug_log!("[joinir_vm_bridge] Phase 27-shortterm S-4.3");
debug_log!("[joinir_vm_bridge] Converting JoinIR to MIR for VM execution");
// Step 1: JoinIR → MIR 変換 (with argument wrapper)
let mir_module = convert_joinir_to_mir_with_args(join_module, entry_func, args)?;
// Step 1: JoinIR → MIR 変換
let mir_module = convert_joinir_to_mir(join_module)?;
debug_log!(
"[joinir_vm_bridge] Converted {} JoinIR functions to MIR (+ entry wrapper)",
"[joinir_vm_bridge] Converted {} JoinIR functions to MIR",
join_module.functions.len()
);
@ -142,11 +128,14 @@ pub fn run_joinir_via_vm(
args.len()
);
let result_box = vm.execute_module(&mir_module)?;
// Convert JoinValue → VMValue (BoxRef 含む)
let vm_args: Vec<VMValue> = args.iter().cloned().map(|v| v.into_vm_value()).collect();
let entry_name = join_func_name(entry_func);
let result = vm.execute_function_with_args(&mir_module, &entry_name, &vm_args)?;
// Step 3: VMValue → JoinValue 変換
let vm_value = VMValue::from_nyash_box(result_box);
let join_result = JoinValue::from_vm_value(&vm_value)
let join_result = JoinValue::from_vm_value(&result)
.map_err(|e| JoinIrVmBridgeError::new(format!("Result conversion error: {}", e.message)))?;
debug_log!("[joinir_vm_bridge] Execution succeeded: {:?}", join_result);
@ -154,17 +143,8 @@ pub fn run_joinir_via_vm(
Ok(join_result)
}
/// Phase 30.x: JoinIR → MIR 変換器 (with entry arguments)
///
/// Creates a wrapper "main" function that:
/// 1. Creates constant values for the entry arguments
/// 2. Calls the actual entry function with those arguments
/// 3. Returns the result
fn convert_joinir_to_mir_with_args(
join_module: &JoinModule,
entry_func: JoinFuncId,
args: &[JoinValue],
) -> Result<MirModule, JoinIrVmBridgeError> {
/// Phase 30.x: JoinIR → MIR 変換器
fn convert_joinir_to_mir(join_module: &JoinModule) -> Result<MirModule, JoinIrVmBridgeError> {
let mut mir_module = MirModule::new("joinir_bridge".to_string());
// Convert all JoinIR functions to MIR (entry function becomes "skip" or similar)
@ -180,75 +160,9 @@ fn convert_joinir_to_mir_with_args(
mir_module.functions.insert(join_func_name(*func_id), mir_func);
}
// Create a wrapper "main" function that calls the entry function with args
let wrapper = create_entry_wrapper(entry_func, args)?;
mir_module.functions.insert("main".to_string(), wrapper);
Ok(mir_module)
}
/// Create a wrapper "main" function that calls the entry function with constant arguments
fn create_entry_wrapper(
entry_func: JoinFuncId,
args: &[JoinValue],
) -> Result<MirFunction, JoinIrVmBridgeError> {
let entry_block = BasicBlockId(0);
let signature = FunctionSignature {
name: "main".to_string(),
params: vec![], // No parameters - args are hardcoded
return_type: MirType::Unknown,
effects: EffectMask::PURE,
};
let mut mir_func = MirFunction::new(signature, entry_block);
// Generate instructions to create constant arguments and call entry function
let mut instructions = Vec::new();
// Create constant values for each argument
let mut arg_value_ids = Vec::new();
for (i, arg) in args.iter().enumerate() {
let dst = ValueId(50000 + i as u32); // Use high ValueIds to avoid conflicts
arg_value_ids.push(dst);
let const_value = join_value_to_mir_const(arg)?;
instructions.push(MirInstruction::Const { dst, value: const_value });
}
// Create function name constant
let func_name_id = ValueId(59990);
instructions.push(MirInstruction::Const {
dst: func_name_id,
value: MirConstValue::String(join_func_name(entry_func)),
});
// Create Call instruction
let result_id = ValueId(59991);
instructions.push(MirInstruction::Call {
dst: Some(result_id),
func: func_name_id,
callee: None,
args: arg_value_ids,
effects: EffectMask::PURE,
});
// Set up entry block
finalize_block(
&mut mir_func,
entry_block,
instructions,
MirInstruction::Return { value: Some(result_id) },
);
debug_log!(
"[joinir_vm_bridge] Created entry wrapper with {} args",
args.len()
);
Ok(mir_func)
}
/// JoinFunction → MirFunction 変換
fn convert_join_function_to_mir(
join_func: &crate::mir::join_ir::JoinFunction,

View File

@ -884,20 +884,6 @@ impl<'a> LoopBuilder<'a> {
}
}
#[allow(dead_code)]
fn add_predecessor(&mut self, block: BasicBlockId, pred: BasicBlockId) -> Result<(), String> {
if let Some(ref mut function) = self.parent_builder.current_function {
if let Some(block) = function.get_block_mut(block) {
block.add_predecessor(pred);
Ok(())
} else {
Err(format!("Block {} not found", block))
}
} else {
Err("No current function".to_string())
}
}
// =============================================================
// Variable Map Utilities — snapshots and rebinding
// =============================================================

View File

@ -30,6 +30,7 @@ pub mod join_ir; // Phase 26-H: 関数正規化IRJoinIR
pub mod join_ir_ops; // Phase 27.8: JoinIR 命令意味箱ops box
pub mod join_ir_runner; // Phase 27.2: JoinIR 実行器(実験用)
pub mod join_ir_vm_bridge; // Phase 27-shortterm S-4: JoinIR → Rust VM ブリッジ
pub mod join_ir_vm_bridge_dispatch; // Phase 30 F-4.4: JoinIR VM ブリッジ dispatch helper
pub mod loop_form; // ControlForm::LoopShape の薄いエイリアス
pub mod optimizer_passes; // optimizer passes (normalize/diagnostics)
pub mod optimizer_stats; // extracted stats struct

View File

@ -168,7 +168,8 @@ impl LocalScopeInspectorBox {
///
/// LoopScopeShape::classify() は既に exit_live 情報を含んでいるため、
/// 直接 classify() → needs_exit_phi() を使う方が効率的。
pub fn is_available_in_all_with_scope(
#[allow(dead_code)] // Phase 30 F-1.3: will be used when LocalScopeInspectorBox is absorbed
pub(crate) fn is_available_in_all_with_scope(
&self,
var_name: &str,
scope: &crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape,

View File

@ -195,7 +195,8 @@ impl LoopExitLivenessBox {
/// let scope = LoopScopeShape::from_existing_boxes(...)?;
/// let live = liveness_box.get_exit_live_from_scope(&scope);
/// ```
pub fn get_exit_live_from_scope(
#[allow(dead_code)] // Phase 30 F-1.2: will be used when LoopExitLivenessBox is absorbed
pub(crate) fn get_exit_live_from_scope(
&self,
scope: &crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape,
) -> BTreeSet<String> {

View File

@ -278,7 +278,8 @@ impl LoopVarClassBox {
/// let scope = LoopScopeShape::from_existing_boxes(...)?;
/// let class = classifier.classify_with_scope("ch", &scope);
/// ```
pub fn classify_with_scope(
#[allow(dead_code)] // Phase 30 F-1.1: will be used when LoopVarClassBox is absorbed
pub(crate) fn classify_with_scope(
&self,
var_name: &str,
scope: &crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape,
@ -287,7 +288,8 @@ impl LoopVarClassBox {
}
/// Phase 30: LoopScopeShape を使って複数変数を一括分類
pub fn classify_all_with_scope(
#[allow(dead_code)] // Phase 30 F-1.1: will be used when LoopVarClassBox is absorbed
pub(crate) fn classify_all_with_scope(
&self,
var_names: &[String],
scope: &crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape,

View File

@ -9,7 +9,9 @@ pub(crate) enum MemberKind {
Method,
Constructor,
PropertyComputed,
#[allow(dead_code)] // Future: once property modifier
PropertyOnce,
#[allow(dead_code)] // Future: birth_once property modifier
PropertyBirthOnce,
}

View File

@ -208,9 +208,8 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
"vm" => {
crate::cli_v!("🚀 Hakorune VM Backend - Executing file: {} 🚀", filename);
// Route to primary VM path by default. Fallback is a last resort and must be explicitly enabled.
let force_fallback =
std::env::var("NYASH_VM_USE_FALLBACK").ok().as_deref() == Some("1");
let route_trace = std::env::var("NYASH_VM_ROUTE_TRACE").ok().as_deref() == Some("1");
let force_fallback = crate::config::env::vm_use_fallback();
let route_trace = crate::config::env::vm_route_trace();
if force_fallback {
if route_trace {
eprintln!("[vm-route] choose=fallback reason=env:NYASH_VM_USE_FALLBACK=1");

View File

@ -579,8 +579,70 @@ pub(super) fn lower_program(
}
pub(super) fn maybe_dump_mir(module: &MirModule) {
// New: file dump path for offline analysis (Stage1/StageB selfhost, ParserBox 等)
// Use env RUST_MIR_DUMP_PATH to write the MIR printer output to a file.
if let Some(path) = crate::config::env::rust_mir_dump_path() {
if let Ok(mut f) = std::fs::File::create(&path) {
let p = MirPrinter::new();
let _ = std::io::Write::write_all(&mut f, p.print_module(module).as_bytes());
}
}
// Existing: verbose flag dumps to stdout
if crate::config::env::cli_verbose() {
let p = MirPrinter::new();
println!("{}", p.print_module(module));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn temp_path(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("{}_{}", name, std::process::id()));
p
}
fn dummy_module() -> MirModule {
MirModule::new("test-module".to_string())
}
#[test]
fn writes_file_when_dump_path_is_set() {
let path = temp_path("mir_dump_path");
let _ = fs::remove_file(&path);
std::env::set_var("RUST_MIR_DUMP_PATH", path.to_string_lossy().to_string());
maybe_dump_mir(&dummy_module());
assert!(
path.exists(),
"maybe_dump_mir should write when RUST_MIR_DUMP_PATH is set"
);
let contents = fs::read_to_string(&path).unwrap_or_default();
assert!(
contents.contains("test-module"),
"dump should contain module name"
);
let _ = fs::remove_file(&path);
std::env::remove_var("RUST_MIR_DUMP_PATH");
}
#[test]
fn does_not_write_when_dump_path_is_unset() {
std::env::remove_var("RUST_MIR_DUMP_PATH");
let path = temp_path("mir_dump_path_unset");
let _ = fs::remove_file(&path);
maybe_dump_mir(&dummy_module());
assert!(
!path.exists(),
"maybe_dump_mir should not write when RUST_MIR_DUMP_PATH is unset"
);
}
}

View File

@ -92,8 +92,11 @@ impl NyashRunner {
return;
}
let groups = self.config.as_groups();
if let Some(code) = self.maybe_run_stage1_cli_stub(&groups) {
std::process::exit(code);
let skip_stage1_stub = groups.emit.hako_emit_program_json || groups.emit.hako_emit_mir_json;
if !skip_stage1_stub {
if let Some(code) = self.maybe_run_stage1_cli_stub(&groups) {
std::process::exit(code);
}
}
// Early: direct MIR JSON execution (no source file). Experimental diagnostics/exec.
if let Some(path) = groups.parser.mir_json_file.as_ref() {
@ -139,6 +142,50 @@ impl NyashRunner {
}
return;
}
// Emit Program(JSON v0) and exit
if let Some(path) = groups.emit.emit_program_json.as_ref() {
// Prefer Stage-1/.hako route when requested via hako-* flags or env
let use_hako = groups.emit.hako_emit_program_json
|| crate::config::env::stage1::emit_program_json()
|| crate::config::env::stage1::enabled();
if use_hako {
if let Err(e) = self.emit_program_json_v0(&groups, path) {
eprintln!("❌ emit-program-json error: {}", e);
std::process::exit(1);
} else {
println!("Program JSON written: {}", path);
std::process::exit(0);
}
} else if let Some(file) = groups.input.file.as_ref() {
match std::fs::read_to_string(file) {
Ok(code) => match crate::parser::NyashParser::parse_from_string(&code) {
Ok(ast) => {
let prog = crate::r#macro::ast_json::ast_to_json(&ast);
let out_path = std::path::Path::new(path);
if let Err(e) = std::fs::write(out_path, prog.to_string()) {
eprintln!("❌ emit-program-json write error: {}", e);
std::process::exit(1);
}
println!("Program JSON written: {}", out_path.display());
std::process::exit(0);
}
Err(e) => {
crate::runner::modes::common_util::diag::print_parse_error_with_context(
file, &code, &e,
);
std::process::exit(1);
}
},
Err(e) => {
eprintln!("❌ Error reading file {}: {}", file, e);
std::process::exit(1);
}
}
} else {
eprintln!("❌ --emit-program-json requires an input file");
std::process::exit(1);
}
}
// Preprocess usings and directives (includes dep-tree log)
self.preprocess_usings_and_directives(&groups);
// JSON v0 bridge

View File

@ -2,13 +2,9 @@ use super::super::NyashRunner;
use nyash_rust::{ast::ASTNode, mir::MirCompiler, parser::NyashParser};
use std::{fs, process};
// Phase 30.x: JoinIR VM Bridge integration (experimental)
// Phase 30 F-4.4: JoinIR VM Bridge dispatch (experimental)
// Used only when NYASH_JOINIR_EXPERIMENT=1 AND NYASH_JOINIR_VM_BRIDGE=1
use crate::config::env::{joinir_experiment_enabled, joinir_vm_bridge_enabled};
use crate::mir::join_ir::{lower_funcscanner_trim_to_joinir, lower_skip_ws_to_joinir, JoinFuncId};
use crate::mir::join_ir::lowering::stage1_using_resolver::lower_stage1_usingresolver_to_joinir;
use crate::mir::join_ir_ops::JoinValue;
use crate::mir::join_ir_vm_bridge::run_joinir_via_vm;
use crate::mir::join_ir_vm_bridge_dispatch::try_run_joinir_vm_bridge;
impl NyashRunner {
/// Execute VM mode with full plugin initialization and AST prelude merge
@ -503,127 +499,10 @@ impl NyashRunner {
}
}
// Phase 30.x: JoinIR VM Bridge experimental path
// Phase 30 F-4.4: JoinIR VM Bridge experimental path (consolidated dispatch)
// Activated when NYASH_JOINIR_EXPERIMENT=1 AND NYASH_JOINIR_VM_BRIDGE=1
// Currently only supports minimal_ssa_skip_ws.hako (Main.skip function)
let joinir_path_attempted = if joinir_experiment_enabled() && joinir_vm_bridge_enabled() {
// Check if this module contains Main.skip/1 (minimal_ssa_skip_ws target)
// Note: function names include arity suffix like "Main.skip/1"
let has_main_skip = module_vm.functions.contains_key("Main.skip/1");
let has_trim = module_vm.functions.contains_key("FuncScannerBox.trim/1");
// Phase 30.x: Stage-1 UsingResolver (deletable block - start)
let has_stage1_usingresolver = module_vm.functions.contains_key("Stage1UsingResolverBox.resolve_for_source/5");
// Phase 30.x: Stage-1 UsingResolver (deletable block - end)
if has_main_skip {
eprintln!("[joinir/vm_bridge] Attempting JoinIR path for Main.skip");
match lower_skip_ws_to_joinir(&module_vm) {
Some(join_module) => {
// Get input argument from NYASH_JOINIR_INPUT or use default
let input = std::env::var("NYASH_JOINIR_INPUT")
.unwrap_or_else(|_| " abc".to_string());
eprintln!("[joinir/vm_bridge] Input: {:?}", input);
match run_joinir_via_vm(
&join_module,
JoinFuncId::new(0),
&[JoinValue::Str(input)],
) {
Ok(result) => {
let exit_code = match &result {
JoinValue::Int(v) => *v as i32,
JoinValue::Bool(b) => if *b { 1 } else { 0 },
_ => 0,
};
eprintln!("[joinir/vm_bridge] ✅ JoinIR result: {:?}", result);
if !quiet_pipe {
println!("RC: {}", exit_code);
}
process::exit(exit_code);
}
Err(e) => {
eprintln!("[joinir/vm_bridge] ❌ JoinIR execution failed: {:?}", e);
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false // Continue to normal VM execution
}
}
}
None => {
eprintln!("[joinir/vm_bridge] lower_skip_ws_to_joinir returned None");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
} else if has_trim {
// Phase 30.x: FuncScannerBox.trim/1 JoinIR path
eprintln!("[joinir/vm_bridge] Attempting JoinIR path for FuncScannerBox.trim");
match lower_funcscanner_trim_to_joinir(&module_vm) {
Some(join_module) => {
// Get input argument from NYASH_JOINIR_INPUT or use default
let input = std::env::var("NYASH_JOINIR_INPUT")
.unwrap_or_else(|_| " abc ".to_string());
eprintln!("[joinir/vm_bridge] Input: {:?}", input);
match run_joinir_via_vm(
&join_module,
JoinFuncId::new(0),
&[JoinValue::Str(input)],
) {
Ok(result) => {
// trim returns a string, print it and exit with 0
eprintln!("[joinir/vm_bridge] ✅ JoinIR trim result: {:?}", result);
if !quiet_pipe {
match &result {
JoinValue::Str(s) => println!("{}", s),
_ => println!("{:?}", result),
}
}
process::exit(0);
}
Err(e) => {
eprintln!("[joinir/vm_bridge] ❌ JoinIR trim failed: {:?}", e);
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
}
None => {
eprintln!("[joinir/vm_bridge] lower_funcscanner_trim_to_joinir returned None");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
// Phase 30.x: Stage-1 UsingResolver JoinIR path (deletable block - start)
} else if has_stage1_usingresolver {
eprintln!("[joinir/vm_bridge] Attempting JoinIR path for Stage1UsingResolverBox.resolve_for_source");
match lower_stage1_usingresolver_to_joinir(&module_vm) {
Some(join_module) => {
eprintln!("[joinir/vm_bridge] ✅ Stage-1 JoinIR module generated ({} functions)", join_module.functions.len());
// Stage-1 requires ArrayBox/MapBox arguments which JoinValue doesn't support yet
// For now, just verify lowering works and fall back to VM
eprintln!("[joinir/vm_bridge] Note: ArrayBox/MapBox args not yet supported in JoinValue");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path for actual execution");
false // Fall back to VM for now
}
None => {
eprintln!("[joinir/vm_bridge] lower_stage1_usingresolver_to_joinir returned None");
eprintln!("[joinir/vm_bridge] Falling back to normal VM path");
false
}
}
// Phase 30.x: Stage-1 UsingResolver JoinIR path (deletable block - end)
} else {
false // No supported JoinIR target function
}
} else {
false
};
// Normal VM execution path (fallback or default)
if joinir_path_attempted {
// This branch is never reached because successful JoinIR path calls process::exit()
unreachable!("JoinIR path should have exited");
}
// Routing logic is centralized in join_ir_vm_bridge_dispatch module
try_run_joinir_vm_bridge(&module_vm, quiet_pipe);
match vm.execute_module(&module_vm) {
Ok(ret) => {

View File

@ -227,15 +227,13 @@ impl NyashRunner {
extra_owned.push("--".to_string());
extra_owned.push("--min-json".to_string());
}
// Phase 28.2 fix: Use Stage-B compiler for file-based compilation.
// Stage-A (_compile_source_to_json_v0) expects source CONTENT, not path.
// Stage-B (StageBMain._do_compile_stage_b) reads files via FileBox.
extra_owned.push("--".to_string());
extra_owned.push("--stage-b".to_string());
// Pass source file path to compiler.hako
// Phase 28.2b fix: Use Stage-A (not Stage-B) for compiler.hako.
// Stage-A expects source CONTENT via `--source <code>`, not a file path.
// Stage-B requires FileBox (plugins), but Rust VM can't execute user-defined
// boxes like StageBMain inside compiler.hako. Stage-A works without plugins.
extra_owned.push("--".to_string());
extra_owned.push("--source".to_string());
extra_owned.push("tmp/ny_parser_input.ny".to_string());
extra_owned.push(code_ref.to_string());
if crate::config::env::ny_compiler_stage3() {
extra_owned.push("--".to_string());
extra_owned.push("--stage3".to_string());

View File

@ -4,6 +4,7 @@
* Constructs stage1_args based on execution mode (emit_program / emit_mir / run).
*/
use crate::config::env::stage1;
use crate::cli::CliGroups;
use serde_json;
use std::process;
@ -15,6 +16,7 @@ pub(super) struct Stage1Args {
pub env_script_args: Option<String>,
pub source_env: Option<String>,
pub progjson_env: Option<String>,
pub emit_mir: bool,
}
/// Build stage1_args based on execution mode
@ -25,23 +27,11 @@ pub(super) struct Stage1Args {
/// - run: run --backend <backend> <source.hako>
pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
// Prefer new env (NYASH_STAGE1_*) and fall back to legacy names to keep compatibility.
let source = std::env::var("NYASH_STAGE1_INPUT")
.ok()
.or_else(|| groups.input.file.as_ref().cloned())
.or_else(|| std::env::var("STAGE1_SOURCE").ok())
.or_else(|| std::env::var("STAGE1_INPUT").ok());
let source = stage1::input_path()
.or_else(|| groups.input.file.as_ref().cloned());
let mode_env = std::env::var("NYASH_STAGE1_MODE")
.ok()
.map(|m| m.to_ascii_lowercase().replace('_', "-"));
let emit_program = matches!(
mode_env.as_deref(),
Some("emit-program") | Some("emit-program-json")
) || std::env::var("STAGE1_EMIT_PROGRAM_JSON").ok().as_deref() == Some("1");
let emit_mir = matches!(
mode_env.as_deref(),
Some("emit-mir") | Some("emit-mir-json")
) || std::env::var("STAGE1_EMIT_MIR_JSON").ok().as_deref() == Some("1");
let emit_program = stage1::emit_program_json();
let emit_mir = stage1::emit_mir_json();
let mut args: Vec<String> = Vec::new();
let mut source_env: Option<String> = None;
@ -57,9 +47,7 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
args.push(src);
source_env = args.last().cloned();
} else if emit_mir {
if let Ok(pjson) = std::env::var("NYASH_STAGE1_PROGRAM_JSON")
.or_else(|_| std::env::var("STAGE1_PROGRAM_JSON"))
{
if let Some(pjson) = stage1::program_json_path() {
args.push("emit".into());
args.push("mir-json".into());
args.push("--from-program-json".into());
@ -81,9 +69,7 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
process::exit(97);
});
args.push("run".into());
let backend = std::env::var("NYASH_STAGE1_BACKEND")
.ok()
.or_else(|| std::env::var("STAGE1_BACKEND").ok())
let backend = stage1::backend_hint()
.unwrap_or_else(|| groups.backend.backend.clone());
args.push("--backend".into());
args.push(backend);
@ -110,5 +96,6 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
env_script_args,
source_env,
progjson_env,
emit_mir,
}
}

View File

@ -4,6 +4,8 @@
* Sets default environment variables for Stage-1 CLI child process.
*/
use crate::config::env;
use crate::config::env::stage1;
use std::process::Command;
/// Configure environment variables for Stage-1 CLI child process
@ -23,12 +25,8 @@ pub(super) fn configure_stage1_env(
// Unified Stage-1 env (NYASH_STAGE1_*) — derive from legacy if unset to keep compatibility.
if std::env::var("NYASH_STAGE1_MODE").is_err() {
if std::env::var("STAGE1_EMIT_PROGRAM_JSON").ok().as_deref() == Some("1") {
cmd.env("NYASH_STAGE1_MODE", "emit-program");
} else if std::env::var("STAGE1_EMIT_MIR_JSON").ok().as_deref() == Some("1") {
cmd.env("NYASH_STAGE1_MODE", "emit-mir");
} else if std::env::var("NYASH_USE_STAGE1_CLI").ok().as_deref() == Some("1") {
cmd.env("NYASH_STAGE1_MODE", "run");
if let Some(m) = stage1::mode() {
cmd.env("NYASH_STAGE1_MODE", m);
}
}
@ -53,17 +51,17 @@ pub(super) fn configure_stage1_env(
// Stage-1 unified input/backend (fallback to legacy)
if std::env::var("NYASH_STAGE1_INPUT").is_err() {
if let Ok(src) = std::env::var("STAGE1_SOURCE") {
if let Some(src) = stage1::input_path() {
cmd.env("NYASH_STAGE1_INPUT", src);
}
}
if std::env::var("NYASH_STAGE1_BACKEND").is_err() {
if let Ok(be) = std::env::var("STAGE1_BACKEND") {
if let Some(be) = stage1::backend_hint().or_else(stage1::backend_alias_warned) {
cmd.env("NYASH_STAGE1_BACKEND", be);
}
}
if std::env::var("NYASH_STAGE1_PROGRAM_JSON").is_err() {
if let Ok(pjson) = std::env::var("STAGE1_PROGRAM_JSON") {
if let Some(pjson) = stage1::program_json_path() {
cmd.env("NYASH_STAGE1_PROGRAM_JSON", pjson);
}
}
@ -78,16 +76,16 @@ pub(super) fn configure_stage1_env(
// Parser toggles
if std::env::var("NYASH_ENABLE_USING").is_err() {
cmd.env("NYASH_ENABLE_USING", "1");
cmd.env("NYASH_ENABLE_USING", if env::enable_using() { "1" } else { "0" });
}
if std::env::var("HAKO_ENABLE_USING").is_err() {
cmd.env("HAKO_ENABLE_USING", "1");
cmd.env("HAKO_ENABLE_USING", if env::enable_using() { "1" } else { "0" });
}
if std::env::var("NYASH_PARSER_STAGE3").is_err() {
cmd.env("NYASH_PARSER_STAGE3", "1");
cmd.env("NYASH_PARSER_STAGE3", if env::parser_stage3() { "1" } else { "0" });
}
if std::env::var("HAKO_PARSER_STAGE3").is_err() {
cmd.env("HAKO_PARSER_STAGE3", "1");
cmd.env("HAKO_PARSER_STAGE3", if env::parser_stage3() { "1" } else { "0" });
}
// Modules list
@ -104,11 +102,11 @@ pub(super) fn configure_stage1_env(
// Backend hint
if std::env::var("STAGE1_BACKEND").is_err() {
if let Some(be) = stage1_args
let be_cli = stage1_args
.windows(2)
.find(|w| w[0] == "--backend")
.map(|w| w[1].clone())
{
.map(|w| w[1].clone());
if let Some(be) = stage1::backend_hint().or(be_cli) {
cmd.env("STAGE1_BACKEND", be);
}
}

View File

@ -20,44 +20,117 @@ mod env;
mod modules;
use super::NyashRunner;
use crate::runner::stage1_bridge::args::Stage1Args;
use crate::config;
use crate::config::env::stage1;
use crate::cli::CliGroups;
use crate::mir::MirPrinter;
use std::io::Write;
use std::path::Path;
impl NyashRunner {
/// Emit Program(JSON v0) using Stage-1 stub and write to a file.
pub(crate) fn emit_program_json_v0(&self, groups: &CliGroups, out_path: &str) -> Result<(), String> {
// Resolve source path from CLI groups or env
let source = stage1::input_path()
.or_else(|| groups.input.file.as_ref().cloned())
.ok_or_else(|| "emit-program-json requires an input file".to_string())?;
// Build minimal args to force emit program-json
let args_result = Stage1Args {
args: vec!["emit".into(), "program-json".into(), source.clone()],
env_script_args: None,
source_env: Some(source.clone()),
progjson_env: None,
emit_mir: false,
};
// Collect modules list (same as bridge)
let modules_list = modules::collect_modules_list();
// Prepare command
let exe = std::env::current_exe().unwrap_or_else(|_| {
std::path::PathBuf::from("target/release/nyash")
});
let mut cmd = std::process::Command::new(exe);
let entry_fn =
std::env::var("NYASH_ENTRY").unwrap_or_else(|_| "Stage1CliMain.main/0".to_string());
let entry = stage1::entry_override()
.unwrap_or_else(|| "lang/src/runner/stage1_cli.hako".to_string());
cmd.arg(&entry).arg("--");
for a in &args_result.args {
cmd.arg(a);
}
// Set environment variables for args
if let Some(src) = args_result.source_env.as_ref() {
cmd.env("STAGE1_SOURCE", src);
cmd.env("NYASH_STAGE1_INPUT", src);
}
if let Ok(text) = std::fs::read_to_string(&source) {
cmd.env("STAGE1_SOURCE_TEXT", text);
}
cmd.env("NYASH_USE_STAGE1_CLI", "1");
// Configure environment (shared helper)
env::configure_stage1_env(&mut cmd, &entry_fn, &args_result.args, modules_list);
cmd.env("STAGE1_EMIT_PROGRAM_JSON", "1");
let output = cmd
.output()
.map_err(|e| format!("stage1 emit program-json spawn failed: {}", e))?;
if !output.stderr.is_empty() && std::env::var("STAGE1_CLI_DEBUG").ok().as_deref() == Some("1") {
let _ = std::io::stderr().write_all(&output.stderr);
}
if !output.status.success() {
if !output.stdout.is_empty() {
let _ = std::io::stdout().write_all(&output.stdout);
}
if !output.stderr.is_empty() {
let _ = std::io::stderr().write_all(&output.stderr);
}
return Err(format!(
"stage1 emit program-json exited with code {:?}",
output.status.code()
));
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let line = crate::runner::modes::common_util::selfhost::json::first_json_v0_line(&stdout)
.ok_or_else(|| "stage1 emit program-json did not produce Program(JSON v0)".to_string())?;
std::fs::write(out_path, line)
.map_err(|e| format!("write {} failed: {}", out_path, e))?;
Ok(())
}
/// If enabled, run the Stage-1 CLI stub as a child process and return its exit code.
/// Returns None when the bridge is not engaged.
pub(crate) fn maybe_run_stage1_cli_stub(&self, groups: &CliGroups) -> Option<i32> {
// Temporary trace: confirm the bridge is evaluated
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") {
if config::env::cli_verbose_level() == 2 {
eprintln!("[stage1-bridge/trace] maybe_run_stage1_cli_stub invoked");
}
// Guard: skip if child invocation
if std::env::var("NYASH_STAGE1_CLI_CHILD").ok().as_deref() == Some("1") {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") {
if stage1::child_invocation() {
if config::env::cli_verbose_level() == 2 {
eprintln!("[stage1-bridge/trace] skip: NYASH_STAGE1_CLI_CHILD=1");
}
return None;
}
// Guard: skip if not enabled
if std::env::var("NYASH_USE_STAGE1_CLI").ok().as_deref() != Some("1") {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") {
if !stage1::enabled() {
if config::env::cli_verbose_level() == 2 {
eprintln!("[stage1-bridge/trace] skip: NYASH_USE_STAGE1_CLI!=1");
}
return None;
}
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1")
|| std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2")
{
if config::env::cli_verbose() || config::env::cli_verbose_level() == 2 {
eprintln!("[stage1-bridge/debug] NYASH_USE_STAGE1_CLI=1 detected");
}
// Locate Stage-1 CLI entry
let entry = std::env::var("STAGE1_CLI_ENTRY")
.or_else(|_| std::env::var("HAKORUNE_STAGE1_ENTRY"))
.unwrap_or_else(|_| "lang/src/runner/stage1_cli.hako".to_string());
let entry = stage1::entry_override()
.unwrap_or_else(|| "lang/src/runner/stage1_cli.hako".to_string());
if !Path::new(&entry).exists() {
eprintln!("[stage1-cli] entry not found: {}", entry);
return Some(97);
@ -109,6 +182,64 @@ impl NyashRunner {
// Configure environment
env::configure_stage1_env(&mut cmd, &entry_fn, &args_result.args, modules_list);
// Emit-mir mode: capture stdout for Program(JSON v0) and lower in Rust to engage maybe_dump_mir.
if args_result.emit_mir {
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
eprintln!("[stage1-cli] failed to spawn stub: {}", e);
return Some(97);
}
};
let code = output.status.code().unwrap_or(1);
if code != 0 {
if !output.stderr.is_empty() {
let _ = std::io::stderr().write_all(&output.stderr);
}
return Some(code);
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let line = match crate::runner::modes::common_util::selfhost::json::first_json_v0_line(&stdout) {
Some(l) => l,
None => {
eprintln!("[stage1-cli] emit-mir: no Program(JSON v0) found in stub output");
return Some(98);
}
};
let module = match super::json_v0_bridge::parse_json_v0_to_module(&line) {
Ok(m) => m,
Err(e) => {
eprintln!("[stage1-cli] emit-mir: Program(JSON v0) parse error: {}", e);
return Some(98);
}
};
super::json_v0_bridge::maybe_dump_mir(&module);
let groups = self.config.as_groups();
if groups.debug.dump_mir {
let mut printer = if groups.debug.mir_verbose {
MirPrinter::verbose()
} else {
MirPrinter::new()
};
if groups.debug.mir_verbose_effects {
printer.set_show_effects_inline(true);
}
println!("{}", printer.print_module(&module));
}
if let Some(path) = groups.emit.emit_mir_json.as_ref() {
let p = std::path::Path::new(path);
if let Err(e) =
crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(&module, p)
{
eprintln!("❌ MIR JSON emit error: {}", e);
return Some(98);
}
println!("MIR JSON written: {}", p.display());
}
return Some(0);
}
crate::cli_v!(
"[stage1-cli] delegating to stub: {} -- {}",
entry,

View File

@ -22,7 +22,7 @@ fn require_experiment_toggle() -> bool {
}
#[test]
#[ignore]
#[ignore] // PHI/LoopForm バグあり - Phase 30 の PHI canary として据え置き
fn joinir_runner_minimal_skip_ws_executes() {
if !require_experiment_toggle() {
return;