Files
hakorune/src/runner/modes/vm_fallback.rs
nyash-codex c479e5f527 fix(dx): Quick Win 1-3 for better error messages and API simplification
Quick Win 1: Show available boxes on "Unknown Box type" error
- vm.rs, vm_fallback.rs: Display sorted list of available user-defined boxes
- Before: "Unknown Box type: Foo"
- After:  "Unknown Box type: Foo. Available: Bar, Baz, Main"

Quick Win 2: Show stderr on child process timeout
- child.rs, selfhost_exe.rs: Capture and display stderr (up to 500 chars)
- Helps diagnose what went wrong in selfhost compiler child process

Quick Win 3: Simplify Stage-B compiler API (SSOT)
- compiler_stageb.hako: Add StageBDriverBox.compile() as single entry point
- compiler_stageb.hako: Remove StageBMain compatibility wrapper
- compiler.hako: Change from `using ... as StageBMain` to direct import
- compiler.hako: Call StageBDriverBox.compile() directly

Also includes child_env.rs NYASH_MODULES env var for module mapping.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 08:44:31 +09:00

564 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::super::NyashRunner;
use crate::{
backend::MirInterpreter,
box_factory::{BoxFactory, RuntimeError},
core::model::BoxDeclaration as CoreBoxDecl,
instance_v2::InstanceBox,
mir::MirCompiler,
parser::NyashParser,
};
use std::sync::{Arc, RwLock};
use std::{fs, process};
impl NyashRunner {
/// Lightweight VM fallback using the in-crate MIR interpreter.
/// - Respects using preprocessing done earlier in the pipeline
/// - Relies on global plugin host initialized by runner
pub(crate) fn execute_vm_fallback_interpreter(&self, filename: &str) {
// Note: hv1 direct route is now handled at main.rs entry point (before plugin initialization).
// This function is only called after plugin initialization has already occurred.
// Read source
let code = match fs::read_to_string(filename) {
Ok(s) => s,
Err(e) => {
eprintln!("❌ Error reading file {}: {}", filename, e);
process::exit(1);
}
};
let trace = crate::config::env::cli_verbose()
|| crate::config::env::env_bool("NYASH_RESOLVE_TRACE");
// Unified using/prelude handling (SSOT, parity with vm.rs):
// - resolve_prelude_paths_profiled で preludes を発見
// - merge_prelude_text で text-merge.hako は AST parse しない)
let mut code2 = if crate::config::env::enable_using() {
match crate::runner::modes::common_util::resolve::resolve_prelude_paths_profiled(
self, &code, filename,
) {
Ok((_, prelude_paths)) => {
if !prelude_paths.is_empty() {
match crate::runner::modes::common_util::resolve::merge_prelude_text(
self, &code, filename,
) {
Ok(merged) => {
if trace {
eprintln!(
"[using/text-merge] preludes={} (vm-fallback)",
prelude_paths.len()
);
}
merged
}
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
} else {
code.clone()
}
}
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
} else {
// using disabled: detect and fail fast if present
if code.contains("\nusing ") || code.trim_start().starts_with("using ") {
eprintln!(
"❌ using: prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines."
);
process::exit(1);
}
code
};
// Dev sugar pre-expand: @name = expr → local name = expr
code2 = crate::runner::modes::common_util::resolve::preexpand_at_local(&code2);
// Hako-friendly normalize
if crate::runner::modes::common_util::hako::looks_like_hako_code(&code2)
|| filename.ends_with(".hako")
{
code2 = crate::runner::modes::common_util::hako::strip_local_decl(&code2);
}
if trace
&& (std::env::var("NYASH_PARSER_STAGE3").ok() == Some("1".into())
|| std::env::var("HAKO_PARSER_STAGE3").ok() == Some("1".into()))
{
eprintln!("[vm-fallback] Stage-3: enabled (env) for {}", filename);
}
// FailFast (optin): Hako 構文を Nyash VM 経路で実行しない
// 目的: .hako は Hakorune VM、MIR は Core/LLVM に役割分離するためのガード
{
let on = crate::runner::modes::common_util::hako::fail_fast_on_hako();
if on {
let s = code2.as_str();
let hako_like = s.contains("static box ")
|| s.contains("using selfhost.")
|| s.contains("using hakorune.");
if hako_like {
eprintln!(
"❌ Hako-like source detected in Nyash VM path. Use Hakorune VM (v1 dispatcher) or Core/LLVM for MIR.\n hint: set HAKO_VERIFY_PRIMARY=hakovm in verify path"
);
process::exit(1);
}
}
}
// Parse main code
let main_ast = match NyashParser::parse_from_string(&code2) {
Ok(ast) => ast,
Err(e) => {
crate::runner::modes::common_util::diag::print_parse_error_with_context(
filename, &code2, &e,
);
process::exit(1);
}
};
// No AST preludes (text path or no using) → use the parsed main AST as-is
let ast_combined = main_ast;
// Optional: dump AST statement kinds for quick diagnostics
if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") {
use nyash_rust::ast::ASTNode;
eprintln!("[ast] dump start (vm-fallback)");
if let ASTNode::Program { statements, .. } = &ast_combined {
for (i, st) in statements.iter().enumerate().take(50) {
let kind = match st {
ASTNode::BoxDeclaration {
is_static, name, ..
} => {
if *is_static {
format!("StaticBox({})", name)
} else {
format!("Box({})", name)
}
}
ASTNode::FunctionDeclaration { name, .. } => format!("FuncDecl({})", name),
ASTNode::FunctionCall { name, .. } => format!("FuncCall({})", name),
ASTNode::MethodCall { method, .. } => format!("MethodCall({})", method),
ASTNode::ScopeBox { .. } => "ScopeBox".to_string(),
ASTNode::ImportStatement { path, .. } => format!("Import({})", path),
ASTNode::UsingStatement { namespace_name, .. } => {
format!("Using({})", namespace_name)
}
_ => format!("{:?}", st),
};
eprintln!("[ast] {}: {}", i, kind);
}
}
eprintln!("[ast] dump end");
}
let ast = crate::r#macro::maybe_expand_and_dump(&ast_combined, false);
// Minimal user-defined Box support (Option A):
// Collect BoxDeclaration entries from AST and register a lightweight
// factory into the unified registry so `new UserBox()` works on the
// VM fallback path as well.
{
use nyash_rust::ast::ASTNode;
// Collect user-defined (non-static) box declarations at program level.
// Additionally, record static box names so we can alias
// `StaticBoxName` -> `StaticBoxNameInstance` when such a
// concrete instance box exists (common pattern in libs).
let mut nonstatic_decls: std::collections::HashMap<String, CoreBoxDecl> =
std::collections::HashMap::new();
let mut static_names: Vec<String> = Vec::new();
if let ASTNode::Program { statements, .. } = &ast {
for st in statements {
if let ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
..
} = st
{
if *is_static {
static_names.push(name.clone());
continue; // modules/static boxes are not user-instantiable directly
}
let decl = CoreBoxDecl {
name: name.clone(),
fields: fields.clone(),
public_fields: public_fields.clone(),
private_fields: private_fields.clone(),
methods: methods.clone(),
constructors: constructors.clone(),
init_fields: init_fields.clone(),
weak_fields: weak_fields.clone(),
is_interface: *is_interface,
extends: extends.clone(),
implements: implements.clone(),
type_parameters: type_parameters.clone(),
};
nonstatic_decls.insert(name.clone(), decl);
}
}
}
// Build final map with optional aliases for StaticName -> StaticNameInstance
let mut decls = nonstatic_decls.clone();
for s in static_names.into_iter() {
let inst = format!("{}Instance", s);
if let Some(d) = nonstatic_decls.get(&inst) {
decls.insert(s, d.clone());
}
}
if !decls.is_empty() {
// Inline factory: minimal User factory backed by collected declarations
struct InlineUserBoxFactory {
decls: Arc<RwLock<std::collections::HashMap<String, CoreBoxDecl>>>,
}
impl BoxFactory for InlineUserBoxFactory {
fn create_box(
&self,
name: &str,
args: &[Box<dyn crate::box_trait::NyashBox>],
) -> Result<Box<dyn crate::box_trait::NyashBox>, RuntimeError>
{
let guard = self.decls.read().unwrap();
let opt = guard.get(name).cloned();
let decl = match opt {
Some(d) => {
drop(guard);
d
}
None => {
// Quick Win 1: Show available boxes for easier debugging
let mut available: Vec<_> = guard.keys().cloned().collect();
available.sort();
drop(guard);
let hint = if available.is_empty() {
"No user-defined boxes available".to_string()
} else if available.len() <= 10 {
format!("Available: {}", available.join(", "))
} else {
format!(
"Available ({} boxes): {}, ...",
available.len(),
available[..10].join(", ")
)
};
return Err(RuntimeError::InvalidOperation {
message: format!("Unknown Box type: {}. {}", name, hint),
});
}
};
let mut inst = InstanceBox::from_declaration(
decl.name.clone(),
decl.fields.clone(),
decl.methods.clone(),
);
let _ = inst.init(args);
Ok(Box::new(inst))
}
fn box_types(&self) -> Vec<&str> {
vec![]
}
fn is_available(&self) -> bool {
true
}
fn factory_type(&self) -> crate::box_factory::FactoryType {
crate::box_factory::FactoryType::User
}
}
let factory = InlineUserBoxFactory {
decls: Arc::new(RwLock::new(decls)),
};
crate::runtime::unified_registry::register_user_defined_factory(Arc::new(factory));
}
}
let mut compiler = MirCompiler::with_options(!self.config.no_optimize);
let compile = match crate::runner::modes::common_util::source_hint::compile_with_source_hint(
&mut compiler,
ast,
Some(filename),
) {
Ok(c) => c,
Err(e) => {
eprintln!("❌ MIR compilation error: {}", e);
process::exit(1);
}
};
// Optional barrier-elision for parity with VM path
let mut module_vm = compile.module.clone();
if crate::config::env::env_bool("NYASH_VM_ESCAPE_ANALYSIS") {
let removed = crate::mir::passes::escape::escape_elide_barriers_vm(&mut module_vm);
if removed > 0 {
crate::cli_v!(
"[VM-fallback] escape_elide_barriers: removed {} barriers",
removed
);
}
}
// Optional: dump MIR for diagnostics (parity with vm path)
// Phase 25.1: File dump for offline analysis (ParserBox等)
if let Ok(path) = std::env::var("RUST_MIR_DUMP_PATH") {
if let Ok(mut f) = std::fs::File::create(&path) {
let p = crate::mir::MirPrinter::new();
let _ = std::io::Write::write_all(&mut f, p.print_module(&module_vm).as_bytes());
eprintln!("[vm-fallback] MIR dumped to: {}", path);
}
}
// Existing: NYASH_VM_DUMP_MIR dumps to stderr
if crate::config::env::env_bool("NYASH_VM_DUMP_MIR") {
let p = crate::mir::MirPrinter::new();
eprintln!("{}", p.print_module(&module_vm));
}
// Execute via MIR interpreter
let mut vm = MirInterpreter::new();
// Centralized plugin guard (non-strict by default on fallback route)
crate::runner::modes::common_util::plugin_guard::check_and_report(
false,
crate::config::env::env_bool("NYASH_JSON_ONLY"),
"vm-fallback",
);
// Optional: verify MIR before execution (dev-only)
if crate::config::env::env_bool("NYASH_VM_VERIFY_MIR") {
let mut verifier = crate::mir::verification::MirVerifier::new();
for (name, func) in module_vm.functions.iter() {
if let Err(errors) = verifier.verify_function(func) {
if !errors.is_empty() {
eprintln!("[vm-verify] function: {}", name);
for er in errors {
eprintln!("{}", er);
}
}
}
}
}
if std::env::var("NYASH_DUMP_FUNCS").ok().as_deref() == Some("1") {
eprintln!("[vm] functions available:");
for k in module_vm.functions.keys() {
eprintln!(" - {}", k);
}
}
match vm.execute_module(&module_vm) {
Ok(ret) => {
use crate::box_trait::{BoolBox, IntegerBox};
// Extract exit code from return value
let exit_code = if let Some(ib) = ret.as_any().downcast_ref::<IntegerBox>() {
ib.value as i32
} else if let Some(bb) = ret.as_any().downcast_ref::<BoolBox>() {
if bb.value {
1
} else {
0
}
} else {
// For non-integer/bool returns, default to 0 (success)
0
};
// Exit with the return value as exit code
process::exit(exit_code);
}
Err(e) => {
eprintln!("❌ VM fallback error: {}", e);
process::exit(1);
}
}
}
}
impl NyashRunner {
/// Small helper to continue fallback execution once AST is prepared
#[allow(dead_code)]
fn execute_vm_fallback_from_ast(&self, filename: &str, ast: nyash_rust::ast::ASTNode) {
use crate::{
backend::MirInterpreter,
box_factory::{BoxFactory, RuntimeError},
core::model::BoxDeclaration as CoreBoxDecl,
instance_v2::InstanceBox,
mir::MirCompiler,
};
use std::process;
use std::sync::{Arc, RwLock};
// Macro expand (if enabled)
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
// Minimal user-defined Box support (inline factory)
{
use nyash_rust::ast::ASTNode;
let mut nonstatic_decls: std::collections::HashMap<String, CoreBoxDecl> =
std::collections::HashMap::new();
let mut static_names: Vec<String> = Vec::new();
if let ASTNode::Program { statements, .. } = &ast {
for st in statements {
if let ASTNode::BoxDeclaration {
name,
fields,
public_fields,
private_fields,
methods,
constructors,
init_fields,
weak_fields,
is_interface,
extends,
implements,
type_parameters,
is_static,
..
} = st
{
if *is_static {
static_names.push(name.clone());
continue;
}
let decl = CoreBoxDecl {
name: name.clone(),
fields: fields.clone(),
public_fields: public_fields.clone(),
private_fields: private_fields.clone(),
methods: methods.clone(),
constructors: constructors.clone(),
init_fields: init_fields.clone(),
weak_fields: weak_fields.clone(),
is_interface: *is_interface,
extends: extends.clone(),
implements: implements.clone(),
type_parameters: type_parameters.clone(),
};
nonstatic_decls.insert(name.clone(), decl);
}
}
}
let mut decls = nonstatic_decls.clone();
for s in static_names.into_iter() {
let inst = format!("{}Instance", s);
if let Some(d) = nonstatic_decls.get(&inst) {
decls.insert(s, d.clone());
}
}
if !decls.is_empty() {
struct InlineUserBoxFactory {
decls: Arc<RwLock<std::collections::HashMap<String, CoreBoxDecl>>>,
}
impl BoxFactory for InlineUserBoxFactory {
fn create_box(
&self,
name: &str,
args: &[Box<dyn crate::box_trait::NyashBox>],
) -> Result<Box<dyn crate::box_trait::NyashBox>, RuntimeError>
{
let guard = self.decls.read().unwrap();
let opt = guard.get(name).cloned();
let decl = match opt {
Some(d) => {
drop(guard);
d
}
None => {
// Quick Win 1: Show available boxes for easier debugging
let mut available: Vec<_> = guard.keys().cloned().collect();
available.sort();
drop(guard);
let hint = if available.is_empty() {
"No user-defined boxes available".to_string()
} else if available.len() <= 10 {
format!("Available: {}", available.join(", "))
} else {
format!(
"Available ({} boxes): {}, ...",
available.len(),
available[..10].join(", ")
)
};
return Err(RuntimeError::InvalidOperation {
message: format!("Unknown Box type: {}. {}", name, hint),
});
}
};
let mut inst = InstanceBox::from_declaration(
decl.name.clone(),
decl.fields.clone(),
decl.methods.clone(),
);
let _ = inst.init(args);
Ok(Box::new(inst))
}
fn box_types(&self) -> Vec<&str> {
vec![]
}
fn is_available(&self) -> bool {
true
}
fn factory_type(&self) -> crate::box_factory::FactoryType {
crate::box_factory::FactoryType::User
}
}
let factory = InlineUserBoxFactory {
decls: Arc::new(RwLock::new(decls)),
};
crate::runtime::unified_registry::register_user_defined_factory(Arc::new(factory));
}
}
// Compile to MIR and execute via interpreter
let mut compiler = MirCompiler::with_options(!self.config.no_optimize);
let module = match crate::runner::modes::common_util::source_hint::compile_with_source_hint(
&mut compiler,
ast,
Some(filename),
) {
Ok(r) => r.module,
Err(e) => {
eprintln!("❌ MIR compilation error: {}", e);
process::exit(1);
}
};
let mut interp = MirInterpreter::new();
match interp.execute_module(&module) {
Ok(result) => {
use nyash_rust::box_trait::{BoolBox, IntegerBox};
let rc = if let Some(ib) = result.as_any().downcast_ref::<IntegerBox>() {
ib.value as i32
} else if let Some(bb) = result.as_any().downcast_ref::<BoolBox>() {
if bb.value {
1
} else {
0
}
} else {
0
};
// For CAPI pure pipeline, suppress "RC:" text to keep last line = exe path
let capi = std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() == Some("1");
let pure = std::env::var("HAKO_CAPI_PURE").ok().as_deref() == Some("1");
if !(capi && pure) {
println!("RC: {}", rc);
}
process::exit(rc);
}
Err(e) => {
eprintln!("❌ VM fallback runtime error: {}", e);
process::exit(1);
}
}
}
}