Merge selfhosting-dev into main (Core-13 pure CI/tests + LLVM bridge) (#126)

* WIP: sync before merging origin/main

* fix: unify using/module + build CLI; add missing helper in runner; build passes; core smokes green; jit any.len string now returns 3

* Apply local changes after merging main; keep docs/phase-15 removed per main; add phase-15.1 docs and tests

* Remove legacy docs/phase-15/README.md to align with main

* integration: add Core-13 pure CI, tests, and minimal LLVM execute bridge (no docs) (#125)

Co-authored-by: Tomoaki <tomoaki@example.com>

---------

Co-authored-by: Selfhosting Dev <selfhost@example.invalid>
Co-authored-by: Tomoaki <tomoaki@example.com>
This commit is contained in:
moe-charm
2025-09-07 07:36:15 +09:00
committed by GitHub
parent 07350c5dd9
commit b8bdb867d8
70 changed files with 2010 additions and 57 deletions

View File

@ -0,0 +1,13 @@
#[cfg(feature = "aot-plan-import")]
#[test]
fn import_plan_v1_min_and_run_vm() {
// Use the embedded minimal plan JSON
let plan = include_str!("../../tools/aot_plan/samples/plan_v1_min.json");
let module = crate::mir::aot_plan_import::import_from_str(plan).expect("import plan v1");
// Execute via VM; expect string "42"
let mut vm = crate::backend::vm::VM::new();
let out = vm.execute_module(&module).expect("vm exec");
assert_eq!(out.to_string_box().value, "42");
}

View File

@ -0,0 +1,19 @@
#[cfg(test)]
mod tests {
use crate::parser::NyashParser;
use crate::backend::VM;
#[test]
fn vm_exec_addition_under_pure_mode() {
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
let code = "\nreturn 7 + 35\n";
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
let mut vm = VM::new();
let out = vm.execute_module(&result.module).expect("vm exec");
assert_eq!(out.to_string_box().value, "42");
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,19 @@
#[cfg(test)]
mod tests {
use crate::parser::NyashParser;
use crate::backend::VM;
#[test]
fn vm_exec_if_then_return_under_pure_mode() {
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
let code = "\nif (1) { return 1 }\nreturn 2\n";
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
let mut vm = VM::new();
let out = vm.execute_module(&result.module).expect("vm exec");
assert_eq!(out.to_string_box().value, "1");
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,30 @@
#[cfg(test)]
mod tests {
use crate::parser::NyashParser;
use crate::backend::VM;
#[test]
fn vm_exec_new_string_length_under_pure_mode() {
// Enable Core-13 pure mode
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
// Nyash code: return (new StringBox("Hello")).length()
let code = r#"
return (new StringBox("Hello")).length()
"#;
// Parse -> MIR -> VM execute
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
let mut vm = VM::new();
let out = vm.execute_module(&result.module).expect("vm exec");
// Expect 5 as string (to_string_box) for convenience
assert_eq!(out.to_string_box().value, "5");
// Cleanup
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,23 @@
mod tests {
use crate::ast::{ASTNode, LiteralValue, Span};
use crate::mir::{MirCompiler, MirPrinter};
#[test]
fn pure_mode_new_emits_env_box_new() {
// Enable pure mode
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
// new StringBox("Hello")
let ast = ASTNode::New {
class: "StringBox".to_string(),
arguments: vec![ASTNode::Literal { value: LiteralValue::String("Hello".into()), span: Span::unknown() }],
type_arguments: vec![],
span: Span::unknown(),
};
let mut c = MirCompiler::new();
let result = c.compile(ast).expect("compile");
let dump = MirPrinter::new().print_module(&result.module);
assert!(dump.contains("extern_call env.box.new"), "expected env.box.new in MIR. dump=\n{}", dump);
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,35 @@
#[cfg(all(test, feature = "llvm"))]
mod tests {
use crate::parser::NyashParser;
use std::fs;
#[test]
fn llvm_can_build_object_under_pure_mode() {
// Enable Core-13 pure mode
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
// A simple program that exercises env.box.new and locals
let code = r#"
local s
s = new StringBox("abc")
return s.length()
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
// Build object via LLVM backend
let out = "nyash_pure_llvm_build_test";
crate::backend::llvm::compile_to_object(&result.module, &format!("{}.o", out)).expect("llvm object build");
// Verify object exists and has content
let meta = fs::metadata(format!("{}.o", out)).expect("obj exists");
assert!(meta.len() > 0, "object file should be non-empty");
// Cleanup
let _ = fs::remove_file(format!("{}.o", out));
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,27 @@
#[cfg(all(test, feature = "llvm"))]
mod tests {
use crate::parser::NyashParser;
use crate::backend::VM;
#[test]
fn llvm_exec_matches_vm_for_addition_under_pure_mode() {
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
let code = "\nreturn 7 + 35\n";
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
// VM result
let mut vm = VM::new();
let vm_out = vm.execute_module(&result.module).expect("vm exec");
let vm_s = vm_out.to_string_box().value;
// LLVM result (compile+execute parity path)
let llvm_out = crate::backend::llvm::compile_and_execute(&result.module, "pure_llvm_parity").expect("llvm exec");
let llvm_s = llvm_out.to_string_box().value;
assert_eq!(vm_s, llvm_s, "VM and LLVM outputs should match");
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,31 @@
#[cfg(test)]
mod tests {
use crate::parser::NyashParser;
use crate::mir::MirPrinter;
#[test]
fn locals_rewritten_to_env_local_calls_in_pure_mode() {
// Enable Core-13 pure mode
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
// Use locals and arithmetic so Load/Store would appear without normalization
let code = r#"
local x
x = 10
x = x + 32
return x
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
let dump = MirPrinter::new().print_module(&result.module);
// Expect env.local.get/set present (pure-mode normalization)
assert!(dump.contains("extern_call env.local.get"), "expected env.local.get in MIR. dump=\n{}", dump);
assert!(dump.contains("extern_call env.local.set"), "expected env.local.set in MIR. dump=\n{}", dump);
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -0,0 +1,48 @@
#[cfg(test)]
mod tests {
use crate::parser::NyashParser;
fn is_allowed_core13(inst: &crate::mir::MirInstruction) -> bool {
use crate::mir::MirInstruction as I;
matches!(inst,
I::Const { .. }
| I::BinOp { .. }
| I::Compare { .. }
| I::Jump { .. }
| I::Branch { .. }
| I::Return { .. }
| I::Phi { .. }
| I::Call { .. }
| I::BoxCall { .. }
| I::ExternCall { .. }
| I::TypeOp { .. }
| I::Safepoint
| I::Barrier { .. }
)
}
#[test]
fn final_mir_contains_only_core13_instructions() {
std::env::set_var("NYASH_MIR_CORE13_PURE", "1");
let code = r#"
local x
x = 1
if (x == 1) { x = x + 41 }
return new StringBox("ok").length()
"#;
let ast = NyashParser::parse_from_string(code).expect("parse");
let mut compiler = crate::mir::MirCompiler::new();
let result = compiler.compile(ast).expect("compile");
// Count non-Core13 instructions
let mut bad = 0usize;
for (_name, f) in &result.module.functions {
for (_bb, b) in &f.blocks {
for i in &b.instructions { if !is_allowed_core13(i) { bad += 1; } }
if let Some(t) = &b.terminator { if !is_allowed_core13(t) { bad += 1; } }
}
}
assert_eq!(bad, 0, "final MIR must contain only Core-13 instructions");
std::env::remove_var("NYASH_MIR_CORE13_PURE");
}
}

View File

@ -22,3 +22,7 @@ pub mod sugar_comp_assign_test;
pub mod sugar_coalesce_test;
pub mod sugar_safe_access_test;
pub mod sugar_range_test;
pub mod policy_mutdeny;
pub mod plugin_hygiene;
#[cfg(feature = "aot-plan-import")]
pub mod aot_plan_import;

View File

@ -0,0 +1,34 @@
#[test]
fn plugin_invoke_hygiene_prefers_hostcall_for_mapped() {
use crate::jit::policy::invoke::{decide_box_method, InvokeDecision};
use crate::jit::r#extern::collections as c;
// Ensure plugin builtins are not forced
std::env::remove_var("NYASH_USE_PLUGIN_BUILTINS");
// For ArrayBox.get, policy should map to hostcall symbol, not plugin invoke
let decision = decide_box_method("ArrayBox", "get", 2, true);
match decision {
InvokeDecision::HostCall { symbol, reason, .. } => {
assert_eq!(symbol, c::SYM_ARRAY_GET_H);
assert_eq!(reason, "mapped_symbol");
}
other => panic!("expected HostCall(mapped_symbol), got: {:?}", other),
}
}
#[test]
fn plugin_invoke_hygiene_string_len_is_hostcall() {
use crate::jit::policy::invoke::{decide_box_method, InvokeDecision};
use crate::jit::r#extern::collections as c;
std::env::remove_var("NYASH_USE_PLUGIN_BUILTINS");
let decision = decide_box_method("StringBox", "len", 1, true);
match decision {
InvokeDecision::HostCall { symbol, reason, .. } => {
assert_eq!(symbol, c::SYM_STRING_LEN_H);
assert_eq!(reason, "mapped_symbol");
}
other => panic!("expected HostCall(mapped_symbol) for String.len, got: {:?}", other),
}
}

106
src/tests/policy_mutdeny.rs Normal file
View File

@ -0,0 +1,106 @@
#[cfg(feature = "cranelift-jit")]
#[test]
#[ignore]
fn jit_readonly_array_push_denied() {
use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask, BasicBlockId, ConstValue, MirType};
// Ensure read-only policy is on
std::env::set_var("NYASH_JIT_READ_ONLY", "1");
// Build: a = new ArrayBox(); a.push(3); ret a.len()
let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE };
let mut f = MirFunction::new(sig, BasicBlockId::new(0));
let bb = f.entry_block;
let a = f.next_value_id();
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::NewBox { dst: a, box_type: "ArrayBox".into(), args: vec![] });
let three = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: three, value: ConstValue::Integer(3) });
// push should be denied under read-only policy, effectively no-op for length
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: a, method: "push".into(), args: vec![three], method_id: None, effects: EffectMask::PURE });
let ln = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: Some(ln), box_val: a, method: "len".into(), args: vec![], method_id: None, effects: EffectMask::PURE });
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(ln) });
let mut m = MirModule::new("jit_readonly_array_push_denied".into()); m.add_function(f);
let out = crate::backend::cranelift_compile_and_execute(&m, "jit_readonly_array_push_denied").expect("JIT exec");
assert_eq!(out.to_string_box().value, "0", "Array.push must be denied under read-only policy");
}
#[cfg(feature = "cranelift-jit")]
#[test]
#[ignore]
fn jit_readonly_map_set_denied() {
use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask, BasicBlockId, ConstValue, MirType};
// Ensure read-only policy is on
std::env::set_var("NYASH_JIT_READ_ONLY", "1");
// Build: m = new MapBox(); m.set("a", 2); ret m.size()
let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE };
let mut f = MirFunction::new(sig, BasicBlockId::new(0));
let bb = f.entry_block;
let mbox = f.next_value_id();
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::NewBox { dst: mbox, box_type: "MapBox".into(), args: vec![] });
let key = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: key, value: ConstValue::String("a".into()) });
let val = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: val, value: ConstValue::Integer(2) });
// set should be denied under read-only policy
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: mbox, method: "set".into(), args: vec![key, val], method_id: None, effects: EffectMask::PURE });
let sz = f.next_value_id(); f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: Some(sz), box_val: mbox, method: "size".into(), args: vec![], method_id: None, effects: EffectMask::PURE });
f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(sz) });
let mut module = MirModule::new("jit_readonly_map_set_denied".into()); module.add_function(f);
let out = crate::backend::cranelift_compile_and_execute(&module, "jit_readonly_map_set_denied").expect("JIT exec");
assert_eq!(out.to_string_box().value, "0", "Map.set must be denied under read-only policy");
}
// Engine-independent smoke: validate policy denial via host externs
#[test]
fn extern_readonly_array_push_denied() {
use std::sync::Arc;
use crate::boxes::array::ArrayBox;
use crate::backend::vm::VMValue;
use crate::jit::r#extern::collections as c;
std::env::set_var("NYASH_JIT_READ_ONLY", "1");
let arr = Arc::new(ArrayBox::new());
let recv = VMValue::BoxRef(arr.clone());
let val = VMValue::Integer(3);
let _ = c::array_push(&[recv.clone(), val]);
let len = c::array_len(&[recv]);
assert_eq!(len.to_string(), "0");
}
#[test]
fn extern_readonly_map_set_denied() {
use std::sync::Arc;
use crate::boxes::map_box::MapBox;
use crate::backend::vm::VMValue;
use crate::jit::r#extern::collections as c;
std::env::set_var("NYASH_JIT_READ_ONLY", "1");
let map = Arc::new(MapBox::new());
let recv = VMValue::BoxRef(map);
let key = VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new("a")));
let val = VMValue::Integer(2);
let _ = c::map_set(&[recv.clone(), key, val]);
let sz = c::map_size(&[recv]);
assert_eq!(sz.to_string(), "0");
}
#[test]
fn extern_readonly_read_ops_allowed() {
use std::sync::Arc;
use crate::boxes::{array::ArrayBox, map_box::MapBox};
use crate::backend::vm::VMValue;
use crate::jit::r#extern::collections as c;
std::env::set_var("NYASH_JIT_READ_ONLY", "1");
// Array: len/get are read-only
let arr = Arc::new(ArrayBox::new());
let recv_a = VMValue::BoxRef(arr.clone());
let len = c::array_len(&[recv_a.clone()]);
assert_eq!(len.to_string(), "0");
let zero = VMValue::Integer(0);
let got = c::array_get(&[recv_a.clone(), zero]);
assert_eq!(got.to_string(), "void");
// Map: size is read-only
let map = Arc::new(MapBox::new());
let recv_m = VMValue::BoxRef(map);
let size = c::map_size(&[recv_m]);
assert_eq!(size.to_string(), "0");
}