Files
endorphin-aster-vm/crates/compiler/tests/pipeline.rs

553 lines
19 KiB
Rust

use compiler::{CompileOptions, CompiledCode, NameBinding, analyze, compile_ast, compile_source};
use endor_core::{
EndorCodeObject, EndorFunctionObject, EndorNumber, ExecutionLimits, NativeFunction, ObjectRef,
Value, Vm, VmError,
};
use frontend::{DiagnosticKind, EndorReader, ParseMode, SourceUnit, parse_ast};
fn compile(text: &str, mode: ParseMode) -> CompiledCode {
compile_source(
&EndorReader::from_text("<test>", text),
mode,
&CompileOptions::default(),
)
.unwrap()
}
fn module(vm: &mut Vm, compiled: &CompiledCode) -> (ObjectRef, ObjectRef) {
let code = compiled.load(vm).unwrap();
let globals = vm.new_dict().unwrap();
let value = vm.execute(code, globals).unwrap();
assert_eq!(value, Value::Nil);
vm.release_value(value).unwrap();
(code, globals)
}
fn function(vm: &Vm, globals: ObjectRef, name: &str) -> ObjectRef {
let Some(Value::Object(function)) = vm.dict_get(globals, &Value::Text(name.into())).unwrap()
else {
panic!("missing function {name}")
};
function
}
fn cleanup(vm: &mut Vm, code: ObjectRef, globals: ObjectRef) {
vm.release(code).unwrap();
vm.dict_clear(globals).unwrap();
vm.release(globals).unwrap();
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn source_files_execute_through_module_and_function_code_objects() {
for (text, expected) in [
(
include_str!("../examples/hello.endor"),
Value::Text("你好,Endor 🌱".into()),
),
(
include_str!("../examples/calls.endor"),
Value::Text("函数调用已接通".into()),
),
(
include_str!("../examples/shadowing.endor"),
Value::Text("局部名称优先".into()),
),
(
include_str!("../examples/number.endor"),
Value::Number(EndorNumber::decimal("-123456789012345678901234567890.00100").unwrap()),
),
] {
let compiled = compile(text, ParseMode::Module);
let mut vm = Vm::new().unwrap();
let (code, globals) = module(&mut vm, &compiled);
let main = function(&vm, globals, "main");
let data = vm.payload::<EndorFunctionObject>(main, "function").unwrap();
assert_eq!(
vm.payload::<EndorCodeObject>(data.code(), "code")
.unwrap()
.code()
.name(),
"main"
);
let result = vm.call(main, &[]).unwrap();
assert_eq!(result, expected);
vm.release_value(result).unwrap();
cleanup(&mut vm, code, globals);
}
}
#[test]
fn expression_mode_preserves_exact_literals_and_signs() {
for (text, expected) in [
(
include_str!("../examples/literal.endor"),
Value::Text("表达式 → 字节码 → VM".into()),
),
(
"-99999999999999999999999999999999999999",
Value::Number(EndorNumber::integer("-99999999999999999999999999999999999999").unwrap()),
),
("--00042", Value::Number(42_i64.into())),
(
"-0.000",
Value::Number(EndorNumber::decimal("-0.000").unwrap()),
),
] {
let mut vm = Vm::new().unwrap();
let code = compile(text, ParseMode::Expression).load(&mut vm).unwrap();
let globals = vm.new_dict().unwrap();
let value = vm.execute(code, globals).unwrap();
assert_eq!(value, expected);
vm.release_value(value).unwrap();
cleanup(&mut vm, code, globals);
}
}
#[test]
fn scope_analysis_resolves_local_shadowing_and_forward_globals() {
let source = "function main(target: Callable): String { let copy = target return later(copy) } function later(value: String): String { return value }";
let root = parse_ast(source, ParseMode::Module).unwrap();
let symbols = analyze(&root, &CompileOptions::default()).unwrap();
assert_eq!(symbols.functions[0].layout.parameters, ["target"]);
assert_eq!(symbols.functions[0].layout.locals, ["target", "copy"]);
assert_eq!(
symbols.functions[0]
.references
.iter()
.map(|reference| reference.binding)
.collect::<Vec<_>>(),
[
NameBinding::Local(0),
NameBinding::Global(0),
NameBinding::Local(1)
]
);
assert!(
symbols
.functions
.iter()
.all(|scope| scope.layout.free_variables.is_empty())
);
}
#[test]
fn semantic_errors_prevent_loading_and_keep_unicode_locations() {
for (text, fragment) in [
(
"function f(): String { return missing }",
"undefined name: missing",
),
("function f(x: T, x: T): T { return x }", "declaration: x"),
(
"function f(x: T): T { let x = 1 return x }",
"declaration: x",
),
(
"function f(): T { let x = 1 let x = 2 return x }",
"declaration: x",
),
(
"function f(): T { return 1 } function f(): T { return 2 }",
"declaration: f",
),
(
"function f(): T { let x = x return x }",
"before initialization: x",
),
(
"function f(): T { let x = later let later = 1 return x }",
"before initialization: later",
),
(
"function later(): T { return 1 } function f(): T { let x = later let later = 1 return x }",
"before initialization: later",
),
] {
let unit = EndorReader::from_text("<invalid>", text);
let error =
compile_source(&unit, ParseMode::Module, &CompileOptions::default()).unwrap_err();
assert_eq!(error.diagnostic.kind, DiagnosticKind::Semantic);
assert!(error.to_string().contains(fragment), "{error}");
}
let unit = EndorReader::from_text("<日本語>", "function 表示(): T {\r\n return 未定義\r\n}");
let error = compile_source(&unit, ParseMode::Module, &CompileOptions::default()).unwrap_err();
assert_eq!((error.location.line, error.location.column), (2, 10));
assert!(error.to_string().starts_with("<日本語>:2:10:"));
}
fn identity(arguments: &[Value]) -> Result<Value, VmError> {
if arguments.len() != 1 {
return Err(VmError::InvalidArgument("identity needs one argument"));
}
Ok(arguments[0].clone())
}
#[test]
fn host_globals_member_calls_and_object_results_keep_owned_roots() {
let options = CompileOptions {
globals: vec!["service".into(), "value".into()],
};
let unit = EndorReader::from_text("<host>", "service.echo(value)");
let compiled = compile_source(&unit, ParseMode::Expression, &options).unwrap();
let mut vm = Vm::new().unwrap();
let native = vm.allocate(NativeFunction::new("echo", identity)).unwrap();
let service = vm.new_object().unwrap();
vm.set_attr(service, "echo", Value::Object(native)).unwrap();
vm.release(native).unwrap();
let object = vm.new_object().unwrap();
let globals = vm.new_dict().unwrap();
vm.dict_set(
globals,
Value::Text("service".into()),
Value::Object(service),
)
.unwrap();
vm.dict_set(globals, Value::Text("value".into()), Value::Object(object))
.unwrap();
vm.release(service).unwrap();
vm.release(object).unwrap();
let code = compiled.load(&mut vm).unwrap();
let result = vm.execute(code, globals).unwrap();
assert_eq!(result, Value::Object(object));
vm.dict_clear(globals).unwrap();
assert!(vm.object(object).is_ok());
vm.release_value(result).unwrap();
assert_eq!(vm.object(object).unwrap_err(), VmError::InvalidHandle);
cleanup(&mut vm, code, globals);
}
#[test]
fn code_templates_load_independently_and_functions_share_loaded_code() {
let compiled = compile(include_str!("../examples/calls.endor"), ParseMode::Module);
let mut left = Vm::new().unwrap();
let mut right = Vm::new().unwrap();
let (left_code, left_globals) = module(&mut left, &compiled);
let (right_code, right_globals) = module(&mut right, &compiled);
assert_eq!(left.object(right_code).unwrap_err(), VmError::ForeignHandle);
let before = left.object_count();
let main = function(&left, left_globals, "main");
for _ in 0..3 {
let result = left.call(main, &[]).unwrap();
left.release_value(result).unwrap();
}
assert_eq!(left.object_count(), before);
cleanup(&mut left, left_code, left_globals);
cleanup(&mut right, right_code, right_globals);
}
#[test]
fn argument_and_recursion_failures_unwind_vm_frames_and_keep_traceback() {
let source =
"\nfunction main(): T { return echo() }\nfunction echo(value: T): T { return value }";
let compiled = compile(source, ParseMode::Module);
let mut vm = Vm::new().unwrap();
let (code, globals) = module(&mut vm, &compiled);
let baseline = vm.object_count();
let main = function(&vm, globals, "main");
assert!(matches!(
vm.call(main, &[]),
Err(VmError::ArgumentCount { provided: 0, .. })
));
assert_eq!(vm.object_count(), baseline);
assert_eq!(vm.last_traceback().last().unwrap().first_line, 2);
cleanup(&mut vm, code, globals);
let mut vm = Vm::new().unwrap();
let (code, globals) = module(
&mut vm,
&compile("function main(): T { return main() }", ParseMode::Module),
);
let main = function(&vm, globals, "main");
assert_eq!(
vm.call_with_limits(
main,
&[],
ExecutionLimits {
max_frames: 4,
max_instructions: 100
}
),
Err(VmError::ExecutionLimit("frames"))
);
assert_eq!(vm.last_traceback().len(), 4);
cleanup(&mut vm, code, globals);
}
#[test]
fn declared_host_names_are_checked_again_at_runtime() {
let source = EndorReader::from_text("<host>", "missing");
let compiled = compile_source(
&source,
ParseMode::Expression,
&CompileOptions {
globals: vec!["missing".into()],
},
)
.unwrap();
let mut vm = Vm::new().unwrap();
let code = compiled.load(&mut vm).unwrap();
let globals = vm.new_dict().unwrap();
assert_eq!(
vm.execute(code, globals),
Err(VmError::NameNotFound("missing".into()))
);
cleanup(&mut vm, code, globals);
}
#[test]
fn interactive_submissions_can_share_a_host_owned_namespace() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
for (text, options, expected) in [
(
"let value = \"保留\"",
CompileOptions::default(),
Value::Nil,
),
(
"value",
CompileOptions {
globals: vec!["value".into()],
},
Value::Text("保留".into()),
),
] {
let source = EndorReader::from_text("<submission>", text);
let compiled = compile_source(&source, ParseMode::Interactive, &options).unwrap();
let code = compiled.load(&mut vm).unwrap();
let result = vm.execute(code, globals).unwrap();
assert_eq!(result, expected);
vm.release_value(result).unwrap();
vm.release(code).unwrap();
}
vm.dict_clear(globals).unwrap();
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn extended_operands_work_for_large_local_and_constant_tables() {
let mut source = String::from("function main(): Number { ");
for index in 0..270 {
source.push_str(&format!("let value{index} = {index} "));
}
source.push_str("return value269 }");
let mut vm = Vm::new().unwrap();
let (code, globals) = module(&mut vm, &compile(&source, ParseMode::Module));
let main = function(&vm, globals, "main");
let result = vm.call(main, &[]).unwrap();
assert_eq!(result, Value::Number(269_i64.into()));
vm.release_value(result).unwrap();
cleanup(&mut vm, code, globals);
}
#[test]
fn unsupported_execution_features_are_compile_errors_instead_of_fake_results() {
let source = EndorReader::from_text("<unsupported>", ".title");
let error =
compile_source(&source, ParseMode::Expression, &CompileOptions::default()).unwrap_err();
assert_eq!(error.diagnostic.kind, DiagnosticKind::Compilation);
}
#[test]
fn compiling_ast_rejects_invalid_source_ranges() {
let source: SourceUnit = EndorReader::from_text("<source>", "\"日\"");
let mut root = parse_ast(source.text(), ParseMode::Expression).unwrap();
if let frontend::ast::AstRoot::Expression(expression) = &mut root {
expression.span.end = 2; // Splits the three-byte character.
}
assert!(compile_ast(&root, &source, &CompileOptions::default()).is_err());
}
#[test]
fn bare_assignments_create_and_replace_real_interactive_globals() {
let mut vm = Vm::new().unwrap();
let globals = vm.new_dict().unwrap();
for text in ["value = \"first\"", "value = \"second\"", "value"] {
let options = CompileOptions {
globals: vm
.dict_items(globals)
.unwrap()
.iter()
.filter_map(|(key, _)| match key {
Value::Text(name) => Some(name.clone()),
_ => None,
})
.collect(),
};
let compiled = compile_source(
&EndorReader::from_text("<session>", text),
ParseMode::Interactive,
&options,
)
.unwrap();
let code = compiled.load(&mut vm).unwrap();
let result = vm.execute(code, globals).unwrap();
if text == "value" {
assert_eq!(result, Value::Text("second".into()));
}
vm.release_value(result).unwrap();
vm.release(code).unwrap();
}
assert_eq!(
vm.dict_get(globals, &Value::Text("value".into())).unwrap(),
Some(Value::Text("second".into()))
);
vm.dict_clear(globals).unwrap();
vm.release(globals).unwrap();
vm.collect_cycles();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn assignments_update_parameters_and_allocate_one_local_slot_per_target() {
let compiled = compile(
"function main(value: String): String { copy = value copy = \"updated\" value = copy return value }",
ParseMode::Module,
);
let mut vm = Vm::new().unwrap();
let (code, globals) = module(&mut vm, &compiled);
let result = vm
.call(
function(&vm, globals, "main"),
&[Value::Text("original".into())],
)
.unwrap();
assert_eq!(result, Value::Text("updated".into()));
vm.release_value(result).unwrap();
cleanup(&mut vm, code, globals);
assert!(
compile_source(
&EndorReader::from_text("<uninitialized>", "function f(): T { x = x return x }"),
ParseMode::Module,
&CompileOptions::default()
)
.is_err()
);
assert!(
compile_source(
&EndorReader::from_text(
"<duplicate>",
"function f(): T { let x = 1 let x = 2 return x }"
),
ParseMode::Module,
&CompileOptions::default()
)
.is_err()
);
}
#[test]
fn source_signatures_survive_template_load_and_enable_runtime_checks() {
let compiled = compile(
"function identity(value: Number): Number { return value }\nfunction bad(): Number { return \"wrong\" }",
ParseMode::Module,
);
let signature = compiled
.children()
.next()
.unwrap()
.source_signature()
.unwrap();
assert_eq!(signature.parameters, ["Number"]);
assert_eq!(signature.return_type, "Number");
let mut vm = Vm::new().unwrap();
vm.set_source_type_checks(true).unwrap();
let (code, globals) = module(&mut vm, &compiled);
let identity = function(&vm, globals, "identity");
assert_eq!(
vm.call(identity, &[Value::Number(7.into())]).unwrap(),
Value::Number(7.into())
);
assert!(matches!(
vm.call(identity, &[Value::Text("wrong".into())]),
Err(VmError::TypeMismatch { .. })
));
let bad = function(&vm, globals, "bad");
let baseline = vm.object_count();
assert!(matches!(
vm.call(bad, &[]),
Err(VmError::TypeMismatch { .. })
));
assert_eq!(vm.object_count(), baseline);
cleanup(&mut vm, code, globals);
}
#[test]
fn source_control_flow_and_exact_arithmetic_execute_real_bytecode() {
let mut vm = Vm::new().unwrap();
let text = "function main(): Number { i=0 total=0 while i<20 { if i<10 {total=total+i} else {total=total-i} i=i+1 } return total }";
let (code, globals) = module(&mut vm, &compile(text, ParseMode::Module));
let main = function(&vm, globals, "main");
let result = vm.call(main, &[]).unwrap();
assert_eq!(result, Value::Number((-100_i64).into()));
vm.release_value(result).unwrap();
cleanup(&mut vm, code, globals);
for (text, expected) in [
("0.1 + 0.2 == 0.3", Value::Bool(true)),
(
"100000000000000000000 + 1 > 100000000000000000000",
Value::Bool(true),
),
("1 / 3 + 1 / 3 + 1 / 3", Value::Number(1_i64.into())),
("-(2 + 3)", Value::Number((-5_i64).into())),
] {
let code = compile(text, ParseMode::Expression).load(&mut vm).unwrap();
let globals = vm.new_dict().unwrap();
let result = vm.execute(code, globals).unwrap();
assert_eq!(result, expected);
vm.release_value(result).unwrap();
vm.release(code).unwrap();
vm.release(globals).unwrap();
}
}
#[test]
fn control_flow_definite_initialization_large_jumps_and_loop_budgets() {
let mut body = String::from("function main(): Number { i=0 while i<2 { if i==0 {");
for _ in 0..300 {
body.push_str("i=i+0 ");
}
body.push_str("} i=i+1 } return i }");
let mut vm = Vm::new().unwrap();
let (code, globals) = module(&mut vm, &compile(&body, ParseMode::Module));
let result = vm.call(function(&vm, globals, "main"), &[]).unwrap();
assert_eq!(result, Value::Number(2_i64.into()));
vm.release_value(result).unwrap();
cleanup(&mut vm, code, globals);
for text in [
"function f(): Number { if 1 {x=2} return x }",
"function f(): Number {while 0 {x=2} return x}",
] {
assert!(
compile_source(
&EndorReader::from_text("<test>", text),
ParseMode::Module,
&CompileOptions::default()
)
.is_err()
);
}
let (code, globals) = module(
&mut vm,
&compile(
"function main(): Unit { while 1 { value=2 } }",
ParseMode::Module,
),
);
assert!(matches!(
vm.call_with_limits(
function(&vm, globals, "main"),
&[],
ExecutionLimits {
max_frames: 10,
max_instructions: 100
}
),
Err(VmError::ExecutionLimit("instructions"))
));
cleanup(&mut vm, code, globals);
}