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:
13
src/tests/aot_plan_import.rs
Normal file
13
src/tests/aot_plan_import.rs
Normal 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");
|
||||
}
|
||||
|
||||
19
src/tests/mir_pure_e2e_arith.rs
Normal file
19
src/tests/mir_pure_e2e_arith.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
19
src/tests/mir_pure_e2e_branch.rs
Normal file
19
src/tests/mir_pure_e2e_branch.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
30
src/tests/mir_pure_e2e_vm.rs
Normal file
30
src/tests/mir_pure_e2e_vm.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
23
src/tests/mir_pure_envbox.rs
Normal file
23
src/tests/mir_pure_envbox.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
35
src/tests/mir_pure_llvm_build.rs
Normal file
35
src/tests/mir_pure_llvm_build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
27
src/tests/mir_pure_llvm_parity.rs
Normal file
27
src/tests/mir_pure_llvm_parity.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
31
src/tests/mir_pure_locals_normalized.rs
Normal file
31
src/tests/mir_pure_locals_normalized.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
48
src/tests/mir_pure_only_core13.rs
Normal file
48
src/tests/mir_pure_only_core13.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
34
src/tests/plugin_hygiene.rs
Normal file
34
src/tests/plugin_hygiene.rs
Normal 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
106
src/tests/policy_mutdeny.rs
Normal 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");
|
||||
}
|
||||
Reference in New Issue
Block a user