use crate::compiler::Compiler;
use crate::op_code::{concat_instructions, Instructions};
use parser::parse;
use std::borrow::Borrow;
use std::rc::Rc;
use object::Object;
pub fn test_constants(expected: &[Object], actual: &Vec<Rc<Object>>) {
assert_eq!(expected.len(), actual.len());
for (exp, b_got) in expected.iter().zip(actual) {
let got = b_got.borrow();
assert_eq!(exp, got);
}
}
#[derive(Debug, Clone)]
pub struct CompilerTestCase<'a> {
pub(crate) input: &'a str,
pub(crate) expected_constants: Vec<Object>,
pub(crate) expected_instructions: Vec<Instructions>,
}
pub fn run_compiler_test(tests: Vec<CompilerTestCase>) {
for t in tests {
let program = parse(t.input).unwrap();
let mut compiler = Compiler::new();
let bytecodes = compiler.compile(&program).unwrap();
test_instructions(&t.expected_instructions, &bytecodes.instructions);
test_constants(&t.expected_constants, &bytecodes.constants);
}
}
fn test_instructions(expected: &Vec<Instructions>, actual: &Instructions) {
let expected_ins = concat_instructions(expected);
assert_eq!(
expected_ins.data.len(),
actual.data.len(),
"instructions length not right\n actual : \n{}\n expected: \n{}",
actual.string(),
expected_ins.string()
);
for (&exp, got) in expected_ins.data.iter().zip(actual.data.clone()) {
assert_eq!(
exp,
got,
"instruction not equal\n actual : \n{}\n expected: \n{}",
actual.string(),
expected_ins.string()
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::op_code::make_instructions;
use crate::op_code::Opcode::*;
use parser::lexer::token::Span;
#[test]
fn integer_arithmetic() {
let tests = vec![
CompilerTestCase {
input: "1 + 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpAdd, &[1]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1; 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpPop, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpPop, &[1]),
],
},
CompilerTestCase {
input: "1 - 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpSub, &[1]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1 * 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpMul, &[1]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "2 / 1",
expected_constants: vec![Object::Integer(2), Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpDiv, &[1]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "-1",
expected_constants: vec![Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpMinus, &[1]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "!true",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpBang, &[1]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn bytecode_string_includes_compiled_function_constants() {
let program = parse("let add = fn(a, b) { a + b };").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let output = bytecode.string();
assert!(output.contains("Instructions:\n0000 OpClosure 0 0\n0004 OpSetGlobal 0\n"));
assert!(output.contains(
"Constants:\n0000 CompiledFunction(name=add, num_locals=2, num_parameters=2)\n"
));
assert!(output.contains(" 0000 OpGetLocal 0\n"));
assert!(output.contains(" 0002 OpGetLocal 1\n"));
assert!(output.contains(" 0004 OpAdd\n"));
assert!(output.contains(" 0005 OpReturnValue\n"));
}
#[test]
fn bytecode_string_labels_anonymous_compiled_functions() {
let program = parse("fn() {};").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
assert!(bytecode.string().contains(
"Constants:\n0000 CompiledFunction(name=<anonymous>, num_locals=0, num_parameters=0)\n"
));
}
#[test]
fn bytecode_tracks_pc_to_source_spans() {
let program = parse("1;\n22").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
assert_eq!(
bytecode.debug_info.pc_spans,
vec![
crate::compiler::PcSpan {
pc: 0,
span: Span {
start: 0,
end: 1
}
},
crate::compiler::PcSpan {
pc: 4,
span: Span {
start: 3,
end: 5
}
},
]
);
assert_eq!(
bytecode.debug_info.span_for_pc(3),
Some(&Span {
start: 0,
end: 1
})
);
assert_eq!(
bytecode.debug_info.span_for_pc(7),
Some(&Span {
start: 3,
end: 5
})
);
}
#[test]
fn bytecode_tracks_function_constant_pc_to_source_spans() {
let input = "let add = fn(a, b) { a + b; };";
let program = parse(input).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let expression_start = input.find("a + b").unwrap();
let function_debug_info = bytecode.function_debug_info.get(&0).unwrap();
assert_eq!(
function_debug_info.span_for_pc(4),
Some(&Span {
start: expression_start,
end: expression_start + "a + b".len()
})
);
}
fn binding(name: &str, slot: usize) -> crate::compiler::BindingDebugInfo {
crate::compiler::BindingDebugInfo {
name: name.to_string(),
slot,
}
}
fn function_constant_index(bytecode: &crate::compiler::Bytecode, name: &str) -> usize {
bytecode
.constants
.iter()
.position(|constant| {
matches!(&**constant, Object::CompiledFunction(function) if function.name == name)
})
.expect("named function constant")
}
#[test]
fn function_debug_info_lists_parameters_then_lets() {
let program = parse("let add = fn(a, b) { let c = a + b; c; };").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let info = bytecode.function_debug_info.get(&0).unwrap();
assert_eq!(info.local_bindings, vec![binding("a", 0), binding("b", 1), binding("c", 2)]);
assert_eq!(info.free_names, Vec::<String>::new());
}
#[test]
fn function_debug_info_orders_free_names_by_capture() {
let program = parse("fn(a, b) { fn() { b + a; }; };").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let inner = bytecode.function_debug_info.get(&0).unwrap();
assert_eq!(inner.local_bindings, vec![]);
assert_eq!(inner.free_names, vec!["b".to_string(), "a".to_string()]);
let outer = bytecode.function_debug_info.get(&1).unwrap();
assert_eq!(outer.local_bindings, vec![binding("a", 0), binding("b", 1)]);
assert_eq!(outer.free_names, Vec::<String>::new());
}
#[test]
fn method_debug_info_lists_this_before_parameters_and_lets() {
let source = "class Point {
constructor(x) { this.value = x; }
scaled(factor) { let result = this.value * factor; result; }
}";
let program = parse(source).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let scaled = function_constant_index(&bytecode, "Point.scaled");
let info = bytecode.function_debug_info.get(&scaled).unwrap();
assert_eq!(
info.local_bindings,
vec![
binding("this", 0),
binding("factor", 1),
binding("result", 2)
]
);
}
#[test]
fn function_debug_info_keeps_rebound_let_slots() {
let program = parse("fn() { let x = 1; let x = 2; x; };").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let function = function_constant_index(&bytecode, "");
let info = bytecode.function_debug_info.get(&function).unwrap();
assert_eq!(info.local_bindings, vec![binding("x", 0), binding("x", 1)]);
}
#[test]
fn main_debug_info_never_lists_bindings() {
let program = parse("let a = 1; let b = 2; let a = 3;").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
assert_eq!(bytecode.debug_info.local_bindings, vec![]);
assert_eq!(bytecode.debug_info.free_names, Vec::<String>::new());
assert_eq!(
compiler.global_bindings(),
vec![binding("a", 0), binding("b", 1), binding("a", 2)]
);
}
#[test]
fn bytecode_debug_view_maps_instruction_lines_to_pc() {
let input = "1;\n22";
let program = parse(input).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let view = bytecode.debug_view();
assert_eq!(view.detail, bytecode.string());
assert_eq!(
view.instruction_lines,
vec![
crate::compiler::InstructionLineMapping {
line: 1,
pc: 0,
scope: crate::compiler::InstructionScope::Main,
},
crate::compiler::InstructionLineMapping {
line: 2,
pc: 3,
scope: crate::compiler::InstructionScope::Main,
},
crate::compiler::InstructionLineMapping {
line: 3,
pc: 4,
scope: crate::compiler::InstructionScope::Main,
},
crate::compiler::InstructionLineMapping {
line: 4,
pc: 7,
scope: crate::compiler::InstructionScope::Main,
},
]
);
}
#[test]
fn bytecode_debug_view_maps_function_instruction_lines() {
let input = "let add = fn(a, b) { a + b; };";
let program = parse(input).unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let view = bytecode.debug_view();
let add_line = view
.instruction_lines
.iter()
.find(|line| {
matches!(
line.scope,
crate::compiler::InstructionScope::Function {
constant_index: 0
}
) && line.pc == 4
})
.expect("OpAdd instruction line");
let expression_start = input.find("a + b").unwrap();
let function_debug_info = view.function_debug_info.get(&0).unwrap();
assert!(add_line.line > 0);
assert_eq!(
function_debug_info.span_for_pc(add_line.pc),
Some(&Span {
start: expression_start,
end: expression_start + "a + b".len(),
})
);
}
#[test]
fn boolean_expression() {
let tests = vec![
CompilerTestCase {
input: "true",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "false",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpFalse, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1 > 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpGreaterThan, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1 < 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpLessThan, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1 == 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpEqual, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "1 != 2",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpNotEqual, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "true == false",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpFalse, &[0]),
make_instructions(OpEqual, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "true != false",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpFalse, &[0]),
make_instructions(OpNotEqual, &[0]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn conditions_only_if() {
let tests = vec![CompilerTestCase {
input: "if (true) { 10 }; 3333;",
expected_constants: vec![Object::Integer(10), Object::Integer(3333)],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpJumpNotTruthy, &[10]),
make_instructions(OpConst, &[0]),
make_instructions(OpJump, &[11]),
make_instructions(OpNull, &[0]),
make_instructions(OpPop, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpPop, &[0]),
],
}];
run_compiler_test(tests);
}
#[test]
fn conditions_with_else() {
let tests = vec![CompilerTestCase {
input: "if (true) { 10 } else { 20 }; 3333;",
expected_constants: vec![
Object::Integer(10),
Object::Integer(20),
Object::Integer(3333),
],
expected_instructions: vec![
make_instructions(OpTrue, &[0]),
make_instructions(OpJumpNotTruthy, &[10]),
make_instructions(OpConst, &[0]),
make_instructions(OpJump, &[13]),
make_instructions(OpConst, &[1]),
make_instructions(OpPop, &[0]),
make_instructions(OpConst, &[2]),
make_instructions(OpPop, &[0]),
],
}];
run_compiler_test(tests);
}
#[test]
fn condition_arms_without_values_emit_null() {
let tests = vec![CompilerTestCase {
input: "if (true) { let y = 1; } else {};",
expected_constants: vec![Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpTrue, &[]),
make_instructions(OpJumpNotTruthy, &[14]),
make_instructions(OpConst, &[0]),
make_instructions(OpSetGlobal, &[0]),
make_instructions(OpNull, &[]),
make_instructions(OpJump, &[15]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
],
}];
run_compiler_test(tests);
}
#[test]
fn debugger_statements_are_completion_transparent() {
let tests = vec![
CompilerTestCase {
input: "debugger;",
expected_constants: vec![],
expected_instructions: vec![make_instructions(OpDebugger, &[])],
},
CompilerTestCase {
input: "1; debugger;",
expected_constants: vec![Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpPop, &[]),
make_instructions(OpDebugger, &[]),
],
},
CompilerTestCase {
input: "if (true) { 1; debugger; }; 3333;",
expected_constants: vec![Object::Integer(1), Object::Integer(3333)],
expected_instructions: vec![
make_instructions(OpTrue, &[]),
make_instructions(OpJumpNotTruthy, &[11]),
make_instructions(OpConst, &[0]),
make_instructions(OpDebugger, &[]),
make_instructions(OpJump, &[12]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
make_instructions(OpConst, &[1]),
make_instructions(OpPop, &[]),
],
},
CompilerTestCase {
input: "if (true) { debugger; }; 3333;",
expected_constants: vec![Object::Integer(3333)],
expected_instructions: vec![
make_instructions(OpTrue, &[]),
make_instructions(OpJumpNotTruthy, &[9]),
make_instructions(OpDebugger, &[]),
make_instructions(OpNull, &[]),
make_instructions(OpJump, &[10]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
make_instructions(OpConst, &[0]),
make_instructions(OpPop, &[]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn test_global_constants() {
let tests = vec![
CompilerTestCase {
input: "let one = 1; let two = 2;",
expected_constants: vec![Object::Integer(1), Object::Integer(2)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpSetGlobal, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpSetGlobal, &[1]),
],
},
CompilerTestCase {
input: "let one = 1; one",
expected_constants: vec![Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpSetGlobal, &[0]),
make_instructions(OpGetGlobal, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "let one = 1; let two = one; two",
expected_constants: vec![Object::Integer(1)],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpSetGlobal, &[0]),
make_instructions(OpGetGlobal, &[0]),
make_instructions(OpSetGlobal, &[1]),
make_instructions(OpGetGlobal, &[1]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn test_string() {
let tests = vec![
CompilerTestCase {
input: "\"monkey\"",
expected_constants: vec![Object::String("monkey".to_string())],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: r#""mon" + "key""#,
expected_constants: vec![
Object::String("mon".to_string()),
Object::String("key".to_string()),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpAdd, &[0]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn test_array() {
let tests = vec![
CompilerTestCase {
input: "[]",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpArray, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "[1, 2, 3]",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(3),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpConst, &[2]),
make_instructions(OpArray, &[3]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "[1 + 2, 3 - 4, 5 * 6]",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(3),
Object::Integer(4),
Object::Integer(5),
Object::Integer(6),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpAdd, &[0]),
make_instructions(OpConst, &[2]),
make_instructions(OpConst, &[3]),
make_instructions(OpSub, &[0]),
make_instructions(OpConst, &[4]),
make_instructions(OpConst, &[5]),
make_instructions(OpMul, &[0]),
make_instructions(OpArray, &[3]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn test_hashmap() {
let tests = vec![
CompilerTestCase {
input: "{}",
expected_constants: vec![],
expected_instructions: vec![
make_instructions(OpHash, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "{1: 2, 3: 4, 5: 6}",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(3),
Object::Integer(4),
Object::Integer(5),
Object::Integer(6),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpConst, &[2]),
make_instructions(OpConst, &[3]),
make_instructions(OpConst, &[4]),
make_instructions(OpConst, &[5]),
make_instructions(OpHash, &[6]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "{1: 2 + 3, 4: 5 * 6}",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(3),
Object::Integer(4),
Object::Integer(5),
Object::Integer(6),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpConst, &[2]),
make_instructions(OpAdd, &[0]),
make_instructions(OpConst, &[3]),
make_instructions(OpConst, &[4]),
make_instructions(OpConst, &[5]),
make_instructions(OpMul, &[0]),
make_instructions(OpHash, &[4]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn test_index() {
let tests = vec![
CompilerTestCase {
input: "[1, 2, 3][1 + 1]",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(3),
Object::Integer(1),
Object::Integer(1),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpConst, &[2]),
make_instructions(OpArray, &[3]),
make_instructions(OpConst, &[3]),
make_instructions(OpConst, &[4]),
make_instructions(OpAdd, &[0]),
make_instructions(OpIndex, &[0]),
make_instructions(OpPop, &[0]),
],
},
CompilerTestCase {
input: "{1: 2 }[2 -1]",
expected_constants: vec![
Object::Integer(1),
Object::Integer(2),
Object::Integer(2),
Object::Integer(1),
],
expected_instructions: vec![
make_instructions(OpConst, &[0]),
make_instructions(OpConst, &[1]),
make_instructions(OpHash, &[2]),
make_instructions(OpConst, &[2]),
make_instructions(OpConst, &[3]),
make_instructions(OpSub, &[0]),
make_instructions(OpIndex, &[0]),
make_instructions(OpPop, &[0]),
],
},
];
run_compiler_test(tests);
}
#[test]
fn compiles_class_constructor_properties_and_new_exactly() {
let constructor = Object::CompiledFunction(Rc::new(object::CompiledFunction {
name: "Point.constructor".to_string(),
instructions: concat_instructions(&vec![
make_instructions(OpGetLocal, &[0]),
make_instructions(OpGetLocal, &[1]),
make_instructions(OpSetProperty, &[1]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
make_instructions(OpGetLocal, &[0]),
make_instructions(OpReturnValue, &[]),
])
.data,
num_locals: 2,
num_parameters: 2,
}));
let value_method = Object::CompiledFunction(Rc::new(object::CompiledFunction {
name: "Point.value".to_string(),
instructions: concat_instructions(&vec![
make_instructions(OpGetLocal, &[0]),
make_instructions(OpGetProperty, &[4]),
make_instructions(OpReturnValue, &[]),
])
.data,
num_locals: 1,
num_parameters: 1,
}));
run_compiler_test(vec![CompilerTestCase {
input: "class Point { constructor(x) { this.value = x; } value() { this.value; } } let point = new Point(1); point.value = 2; point.value;",
expected_constants: vec![
Object::String("Point".to_string()),
Object::String("value".to_string()),
constructor,
Object::String("constructor".to_string()),
Object::String("value".to_string()),
value_method,
Object::String("value".to_string()),
Object::Integer(1),
Object::Integer(2),
Object::String("value".to_string()),
Object::String("value".to_string()),
],
expected_instructions: vec![
make_instructions(OpClass, &[0]),
make_instructions(OpClosure, &[2, 0]),
make_instructions(OpMethod, &[3, 1]),
make_instructions(OpClosure, &[5, 0]),
make_instructions(OpMethod, &[6, 0]),
make_instructions(OpSetGlobal, &[0]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
make_instructions(OpGetGlobal, &[0]),
make_instructions(OpConst, &[7]),
make_instructions(OpNew, &[1]),
make_instructions(OpSetGlobal, &[1]),
make_instructions(OpGetGlobal, &[1]),
make_instructions(OpConst, &[8]),
make_instructions(OpSetProperty, &[9]),
make_instructions(OpNull, &[]),
make_instructions(OpPop, &[]),
make_instructions(OpGetGlobal, &[1]),
make_instructions(OpGetProperty, &[10]),
make_instructions(OpPop, &[]),
],
}]);
}
#[test]
fn compiles_this_through_each_nested_closure_scope() {
let program = parse("class Box { reader() { fn() { fn() { this.value; }; }; } }").unwrap();
let mut compiler = Compiler::new();
let bytecode = compiler.compile(&program).unwrap();
let compiled = |index: usize| match bytecode.constants[index].as_ref() {
Object::CompiledFunction(function) => function,
value => panic!("constant {} should be a compiled function, got {:?}", index, value),
};
assert_eq!(
compiled(2).instructions,
concat_instructions(&vec![
make_instructions(OpGetFree, &[0]),
make_instructions(OpGetProperty, &[1]),
make_instructions(OpReturnValue, &[]),
])
.data
);
assert_eq!(
compiled(3).instructions,
concat_instructions(&vec![
make_instructions(OpGetFree, &[0]),
make_instructions(OpClosure, &[2, 1]),
make_instructions(OpReturnValue, &[]),
])
.data
);
assert_eq!(
compiled(4).instructions,
concat_instructions(&vec![
make_instructions(OpGetLocal, &[0]),
make_instructions(OpClosure, &[3, 1]),
make_instructions(OpReturnValue, &[]),
])
.data
);
assert_eq!((compiled(4).num_locals, compiled(4).num_parameters), (1, 1));
}
#[test]
fn validation_accepts_globals_from_previous_compiler_state() {
let mut first = Compiler::new();
first.compile(&parse("let answer = 41;").unwrap()).unwrap();
let mut next = Compiler::new_with_state(first.symbol_table, first.constants);
next.compile(&parse("answer + 1;").unwrap()).unwrap();
}
fn compile_error(input: &str) -> String {
let mut compiler = Compiler::new();
return compiler
.compile(&parse(input).unwrap())
.expect_err("expected the program to be rejected");
}
fn repeat_joined(count: usize, item: impl Fn(usize) -> String) -> String {
return (0..count).map(item).collect::<Vec<_>>().join(" ");
}
fn comma_list(count: usize, item: impl Fn(usize) -> String) -> String {
return (0..count).map(item).collect::<Vec<_>>().join(", ");
}
#[test]
fn rejects_too_many_locals() {
let lets = repeat_joined(257, |i| format!("let v{i} = {i};"));
let input = format!("let f = fn() {{ {lets} v0 }}; f()");
assert_eq!(compile_error(&input), "too many locals: 257 exceeds the maximum of 256");
}
#[test]
fn accepts_max_locals() {
let lets = repeat_joined(256, |i| format!("let v{i} = {i};"));
let input = format!("let f = fn() {{ {lets} v255 }}; f()");
let mut compiler = Compiler::new();
compiler.compile(&parse(&input).unwrap()).unwrap();
}
#[test]
fn rejects_too_many_globals() {
let lets = repeat_joined(u16::MAX as usize + 2, |i| format!("let g{i} = true;"));
assert_eq!(compile_error(&lets), "too many globals: 65537 exceeds the maximum of 65536");
}
#[test]
fn rejects_too_many_constants() {
let mut compiler = Compiler::new();
for i in 0..=u16::MAX as i64 {
compiler.try_add_constant(Object::Integer(i)).unwrap();
}
assert_eq!(
compiler.try_add_constant(Object::Integer(0)).unwrap_err(),
"too many constants: 65537 exceeds the maximum of 65536"
);
}
#[test]
fn add_constant_keeps_its_published_signature() {
let mut compiler = Compiler::new();
let index: usize = compiler.add_constant(Object::Integer(1));
assert_eq!(index, 0);
}
#[test]
fn rejects_too_many_call_arguments() {
let args = comma_list(256, |i| i.to_string());
let input = format!("let f = fn() {{ 1 }}; f({args})");
assert_eq!(
compile_error(&input),
"too many call arguments: 256 exceeds the maximum of 255"
);
}
#[test]
fn rejects_too_many_constructor_arguments() {
let args = comma_list(256, |i| i.to_string());
let input = format!("class C {{ constructor() {{ 1 }} }} new C({args})");
assert_eq!(
compile_error(&input),
"too many constructor arguments: 256 exceeds the maximum of 255"
);
}
#[test]
fn rejects_too_many_array_elements() {
let elements = comma_list(u16::MAX as usize + 1, |_| "true".to_string());
let input = format!("[{elements}]");
assert_eq!(
compile_error(&input),
"too many array elements: 65536 exceeds the maximum of 65535"
);
}
#[test]
fn rejects_too_many_hash_pairs() {
let entries = comma_list(u16::MAX as usize / 2 + 1, |i| format!("{i}: true"));
let input = format!("{{{entries}}}");
assert_eq!(
compile_error(&input),
"too many hash pairs: 32768 exceeds the maximum of 32767"
);
}
#[test]
fn accepts_max_hash_pairs() {
let entries = comma_list(u16::MAX as usize / 2, |i| format!("{i}: true"));
let mut compiler = Compiler::new();
compiler
.compile(&parse(&format!("{{{entries}}}")).unwrap())
.unwrap();
}
#[test]
fn rejects_too_many_free_variables() {
let lets = repeat_joined(256, |i| format!("let v{i} = {i};"));
let names = (0..256)
.map(|i| format!("v{i}"))
.collect::<Vec<_>>()
.join(" + ");
let expected = "too many free variables: 256 exceeds the maximum of 255";
assert_eq!(
compile_error(&format!("let mk = fn() {{ {lets} fn() {{ {names} }} }};")),
expected
);
assert_eq!(
compile_error(&format!(
"let mk = fn() {{ {lets} fn() {{ let w = 0; fn() {{ {names} + w }} }} }};"
)),
expected
);
}
#[test]
fn rejects_code_larger_than_the_jump_range() {
let body = repeat_joined(20000, |i| format!("{};", i % 9));
let input = format!("if (true) {{ {body} 1 }} else {{ 2 }}");
let err = compile_error(&input);
assert!(
err.starts_with("compiled code too large: jump target at byte ")
&& err.ends_with("is outside the 65535-byte range of a jump operand"),
"unexpected error: {}",
err
);
}
}