use pretty_assertions::assert_eq;
use crate::{
AddressingMode, Assembler, AssemblyError, DropKind, Function, Immediate,
Instruction, RegisterIndex, RollingRecordIndex,
support::{compile_valid, read_compilation_test_cases}
};
#[test]
fn test_text_round_trip_full_optimization()
{
let corpus = include_str!("../../tests/test_full_optimization.txt");
for (index, (source, expected)) in
read_compilation_test_cases(corpus).iter().enumerate()
{
let function = match Assembler::assemble(expected)
{
Ok(f) => f,
Err(e) =>
{
panic!(
"case {}: assemble failed for source `{}`:\n{}\nexpected:\n{}",
index + 1,
source,
e,
expected
);
}
};
let actual = format!("{}", function);
assert_eq!(
actual.trim(),
*expected,
"text round-trip mismatch for case {} source `{}`",
index + 1,
source
);
}
}
#[test]
fn test_struct_round_trip_full_optimization()
{
let corpus = include_str!("../../tests/test_full_optimization.txt");
for (index, (source, _expected)) in
read_compilation_test_cases(corpus).iter().enumerate()
{
let compiled = compile_valid(source);
let text = format!("{}", compiled);
let reassembled = match Assembler::assemble(&text)
{
Ok(f) => f,
Err(e) => panic!(
"case {} source `{}`: re-assemble failed: {}\ntext:\n{}",
index + 1,
source,
e,
text
)
};
assert_eq!(
reassembled,
compiled,
"struct round-trip mismatch for case {} source `{}`",
index + 1,
source
);
}
}
#[test]
fn test_text_round_trip_compile_unoptimized()
{
let corpus = include_str!("../../tests/test_compile_unoptimized.txt");
for (index, (source, expected)) in
read_compilation_test_cases(corpus).iter().enumerate()
{
let function = match Assembler::assemble(expected)
{
Ok(f) => f,
Err(e) => panic!(
"case {} source `{}`: assemble failed: {}\nexpected:\n{}",
index + 1,
source,
e,
expected
)
};
let actual = format!("{}", function);
assert_eq!(
actual.trim(),
*expected,
"text round-trip mismatch for case {} source `{}`",
index + 1,
source
);
}
}
#[test]
fn test_assemble_constant_return()
{
let text = "\
Function() r#0 ⚅#0
\textern[]
\tbody:
\t\treturn 0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(function.parameters, Vec::<String>::new());
assert_eq!(function.externals, Vec::<String>::new());
assert_eq!(function.register_count, 0);
assert_eq!(function.rolling_record_count, 0);
assert_eq!(
function.instructions,
vec![Instruction::r#return(AddressingMode::Immediate(Immediate(
0
)))]
);
}
#[test]
fn test_assemble_single_parameter()
{
let text = "\
Function(x@0) r#1 ⚅#0
\textern[]
\tbody:
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(function.parameters, vec!["x".to_string()]);
assert_eq!(function.register_count, 1);
}
#[test]
fn test_assemble_single_extern()
{
let text = "\
Function() r#1 ⚅#0
\textern[y@0]
\tbody:
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(function.parameters, Vec::<String>::new());
assert_eq!(function.externals, vec!["y".to_string()]);
assert_eq!(function.register_count, 1);
}
#[test]
fn test_assemble_unicode_and_whitespace_names()
{
let text = "\
Function(an argument@0, qualified.access@1, kebab-case@2) r#4 ⚅#0
\textern[выражение в кости@3]
\tbody:
\t\t@3 <- @0 + @3
\t\treturn @3
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.parameters,
vec![
"an argument".to_string(),
"qualified.access".to_string(),
"kebab-case".to_string()
]
);
assert_eq!(function.externals, vec!["выражение в кости".to_string()]);
assert_eq!(function.register_count, 4);
}
#[test]
fn test_assemble_binary_arithmetic()
{
for (sigil, constructor) in [
(
"+",
Instruction::add
as fn(
RegisterIndex,
AddressingMode,
AddressingMode
) -> Instruction
),
("-", Instruction::sub),
("*", Instruction::mul),
("/", Instruction::div),
("%", Instruction::r#mod),
("^", Instruction::exp)
]
{
let text = format!(
"\
Function() r#1 ⚅#0
\textern[]
\tbody:
\t\t@0 <- 3 {} 4
\t\treturn @0
",
sigil
);
let function = Assembler::assemble(&text).unwrap();
assert_eq!(
function.instructions[0],
constructor(
RegisterIndex(0),
AddressingMode::Immediate(Immediate(3)),
AddressingMode::Immediate(Immediate(4))
),
"binary op {} mis-parsed",
sigil
);
}
}
#[test]
fn test_assemble_unary_negation()
{
let text = "\
Function(x@0) r#2 ⚅#0
\textern[]
\tbody:
\t\t@1 <- -@0
\t\treturn @1
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::neg(
RegisterIndex(1),
AddressingMode::Register(RegisterIndex(0))
)
);
let text = "\
Function() r#1 ⚅#0
\textern[]
\tbody:
\t\t@0 <- -5
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::neg(
RegisterIndex(0),
AddressingMode::Immediate(Immediate(5))
)
);
}
#[test]
fn test_assemble_roll_range()
{
let text = "\
Function() r#1 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll range -10:10
\t\t@0 <- sum rolling record ⚅0
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::roll_range(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(-10)),
AddressingMode::Immediate(Immediate(10))
)
);
let text = "\
Function(a@0, b@1) r#3 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll range @0:@1
\t\t@2 <- sum rolling record ⚅0
\t\treturn @2
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::roll_range(
RollingRecordIndex(0),
AddressingMode::Register(RegisterIndex(0)),
AddressingMode::Register(RegisterIndex(1))
)
);
}
#[test]
fn test_assemble_roll_standard_dice()
{
let text = "\
Function() r#1 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll standard dice 3D6
\t\t@0 <- sum rolling record ⚅0
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::roll_standard_dice(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(3)),
AddressingMode::Immediate(Immediate(6))
)
);
}
#[test]
fn test_assemble_roll_custom_dice()
{
let text = "\
Function() r#1 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll custom dice 1D[-1, 0, 1]
\t\t@0 <- sum rolling record ⚅0
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::roll_custom_dice(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(1)),
vec![-1, 0, 1]
)
);
let text = "\
Function() r#1 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll custom dice 2D[42]
\t\t@0 <- sum rolling record ⚅0
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::roll_custom_dice(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(2)),
vec![42]
)
);
}
#[test]
fn test_assemble_drop_clauses()
{
let text = "\
Function() r#1 ⚅#1
\textern[]
\tbody:
\t\t⚅0 <- roll standard dice 3D6
\t\t⚅0 <- drop lowest 1 from ⚅0
\t\t⚅0 <- drop highest 1 from ⚅0
\t\t@0 <- sum rolling record ⚅0
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[1],
Instruction::drop_lowest(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(1))
)
);
assert_eq!(
function.instructions[2],
Instruction::drop_highest(
RollingRecordIndex(0),
AddressingMode::Immediate(Immediate(1))
)
);
}
#[test]
fn test_assemble_negative_operands()
{
let text = "\
Function() r#1 ⚅#0
\textern[]
\tbody:
\t\t@0 <- -5 + -3
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::add(
RegisterIndex(0),
AddressingMode::Immediate(Immediate(-5)),
AddressingMode::Immediate(Immediate(-3))
)
);
let text = "\
Function() r#1 ⚅#0
\textern[]
\tbody:
\t\t@0 <- 10 - -3
\t\treturn @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::sub(
RegisterIndex(0),
AddressingMode::Immediate(Immediate(10)),
AddressingMode::Immediate(Immediate(-3))
)
);
}
#[test]
fn test_assemble_permissive_whitespace()
{
let text = "\
Function(x@0, y@1) r#2 ⚅#0
extern[]
body:
@0 <- @0 + @1
return @0
";
let function = Assembler::assemble(text).unwrap();
assert_eq!(function.parameters, vec!["x".to_string(), "y".to_string()]);
assert_eq!(
function.instructions[0],
Instruction::add(
RegisterIndex(0),
AddressingMode::Register(RegisterIndex(0)),
AddressingMode::Register(RegisterIndex(1))
)
);
}
#[test]
fn test_assemble_crlf_line_endings()
{
let text =
"Function() r#0 ⚅#0\r\n\textern[]\r\n\tbody:\r\n\t\treturn 1\r\n";
let function = Assembler::assemble(text).unwrap();
assert_eq!(
function.instructions[0],
Instruction::r#return(AddressingMode::Immediate(Immediate(1)))
);
}
#[test]
fn test_assemble_surrounding_blank_lines()
{
let text = "\n\n\
Function() r#0 ⚅#0
\textern[]
\tbody:
\t\treturn 0
\n\n";
let function = Assembler::assemble(text).unwrap();
assert_eq!(function.register_count, 0);
}
#[track_caller]
fn assert_rejects(input: &str) -> AssemblyError
{
match Assembler::assemble(input)
{
Ok(function) => panic!(
"expected rejection but assembled successfully:\n{:#?}",
function
),
Err(e) => e
}
}
#[test]
fn test_reject_missing_function_keyword()
{
assert!(matches!(
assert_rejects(
"NotFunction() r#0 ⚅#0\n\textern[]\n\tbody:\n\t\treturn 0\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_missing_register_count_prefix()
{
assert!(matches!(
assert_rejects("Function() 0 ⚅#0\n\textern[]\n\tbody:\n\t\treturn 0\n"),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_missing_rolling_record_count_prefix()
{
assert!(matches!(
assert_rejects("Function() r#0 0\n\textern[]\n\tbody:\n\t\treturn 0\n"),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_missing_extern_line()
{
assert!(matches!(
assert_rejects("Function() r#0 ⚅#0\n\tbody:\n\t\treturn 0\n"),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_missing_body_header()
{
assert!(matches!(
assert_rejects("Function() r#0 ⚅#0\n\textern[]\n\t\treturn 0\n"),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_unknown_opcode()
{
assert!(matches!(
assert_rejects(
"Function() r#1 ⚅#0\n\textern[]\n\tbody:\n\t\t@0 <- wibble 1 + 2\n\t\treturn @0\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_wrong_destination_kind()
{
assert!(matches!(
assert_rejects(
"Function() r#1 ⚅#1\n\textern[]\n\tbody:\n\t\t@0 <- roll range 0:5\n\t\treturn @0\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_rolling_record_as_arithmetic_operand()
{
assert!(matches!(
assert_rejects(
"Function() r#1 ⚅#1\n\textern[]\n\tbody:\n\t\t⚅0 <- roll standard dice 3D6\n\t\t@0 <- ⚅0 + 1\n\t\treturn @0\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_register_out_of_bounds()
{
let e = assert_rejects(
"Function() r#1 ⚅#0\n\textern[]\n\tbody:\n\t\t@5 <- 1 + 2\n\t\treturn @5\n"
);
let AssemblyError::RegisterOutOfBounds {
index,
register_count,
..
} = e
else
{
panic!("expected RegisterOutOfBounds, got: {:?}", e);
};
assert_eq!(index, 5);
assert_eq!(register_count, 1);
}
#[test]
fn test_reject_rolling_record_out_of_bounds()
{
let e = assert_rejects(
"Function() r#1 ⚅#1\n\textern[]\n\tbody:\n\t\t⚅5 <- roll standard dice 3D6\n\t\t@0 <- sum rolling record ⚅5\n\t\treturn @0\n"
);
let AssemblyError::RollingRecordOutOfBounds {
index,
rolling_record_count,
..
} = e
else
{
panic!("expected RollingRecordOutOfBounds, got: {:?}", e);
};
assert_eq!(index, 5);
assert_eq!(rolling_record_count, 1);
}
#[test]
fn test_reject_register_gap()
{
let e = assert_rejects(
"Function() r#3 ⚅#0\n\textern[]\n\tbody:\n\t\t@0 <- 1 + 2\n\t\t@2 <- @0 + 3\n\t\treturn @2\n"
);
let AssemblyError::RegisterGap {
index,
register_count,
..
} = e
else
{
panic!("expected RegisterGap, got: {:?}", e);
};
assert_eq!(index, 1);
assert_eq!(register_count, 3);
}
#[test]
fn test_reject_rolling_record_gap()
{
let e = assert_rejects(
"Function() r#1 ⚅#3\n\textern[]\n\tbody:\n\t\t⚅0 <- roll standard dice 3D6\n\t\t⚅2 <- roll standard dice 3D6\n\t\t@0 <- sum rolling record ⚅2\n\t\treturn @0\n"
);
let AssemblyError::RollingRecordGap {
index,
rolling_record_count,
..
} = e
else
{
panic!("expected RollingRecordGap, got: {:?}", e);
};
assert_eq!(index, 1);
assert_eq!(rolling_record_count, 3);
}
#[test]
fn test_reject_non_contiguous_parameter_indices()
{
let e = assert_rejects(
"Function(x@0, y@2) r#2 ⚅#0\n\textern[]\n\tbody:\n\t\t@0 <- @0 + @1\n\t\treturn @0\n"
);
let AssemblyError::NonContiguousParameter {
name,
index,
position,
..
} = e
else
{
panic!("expected NonContiguousParameter, got: {:?}", e);
};
assert_eq!(name, "y");
assert_eq!(index, 2);
assert_eq!(position, 1);
}
#[test]
fn test_reject_non_contiguous_extern_indices()
{
let e = assert_rejects(
"Function(x@0) r#3 ⚅#0\n\textern[a@1, b@5]\n\tbody:\n\t\t@2 <- @1 + @0\n\t\treturn @2\n"
);
let AssemblyError::NonContiguousExternal {
name,
index,
expected,
arity,
..
} = e
else
{
panic!("expected NonContiguousExternal, got: {:?}", e);
};
assert_eq!(name, "b");
assert_eq!(index, 5);
assert_eq!(expected, 2);
assert_eq!(arity, 1);
}
#[test]
fn test_reject_parameter_index_disagrees_with_position()
{
let e = assert_rejects(
"Function(x@1) r#1 ⚅#0\n\textern[]\n\tbody:\n\t\treturn @0\n"
);
let AssemblyError::NonContiguousParameter {
name,
index,
position,
..
} = e
else
{
panic!("expected NonContiguousParameter, got: {:?}", e);
};
assert_eq!(name, "x");
assert_eq!(index, 1);
assert_eq!(position, 0);
}
#[test]
fn test_reject_faceless_custom_dice()
{
assert!(matches!(
assert_rejects(
"Function() r#1 ⚅#1\n\textern[]\n\tbody:\n\t\t⚅0 <- roll custom dice 1D[]\n\t\t@0 <- sum rolling record ⚅0\n\t\treturn @0\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_drop_source_disagrees_with_destination()
{
let e = assert_rejects(
"Function() r#1 ⚅#2\n\textern[]\n\tbody:\n\t\t⚅0 <- roll standard dice 3D6\n\t\t⚅1 <- roll standard dice 3D6\n\t\t⚅0 <- drop lowest 1 from ⚅1\n\t\t@0 <- sum rolling record ⚅0\n\t\treturn @0\n"
);
let AssemblyError::DropSourceMismatch {
kind,
destination,
source,
..
} = e
else
{
panic!("expected DropSourceMismatch, got: {:?}", e);
};
assert_eq!(kind, DropKind::Lowest);
assert_eq!(destination, 0);
assert_eq!(source, 1);
}
#[test]
fn test_reject_drop_highest_source_disagrees_with_destination()
{
let e = assert_rejects(
"Function() r#1 ⚅#2\n\textern[]\n\tbody:\n\t\t⚅0 <- roll standard dice 3D6\n\t\t⚅1 <- roll standard dice 3D6\n\t\t⚅0 <- drop highest 1 from ⚅1\n\t\t@0 <- sum rolling record ⚅0\n\t\treturn @0\n"
);
let AssemblyError::DropSourceMismatch { kind, .. } = e
else
{
panic!("expected DropSourceMismatch, got: {:?}", e);
};
assert_eq!(kind, DropKind::Highest);
}
#[test]
fn test_reject_trailing_garbage()
{
assert!(matches!(
assert_rejects(
"Function() r#0 ⚅#0\n\textern[]\n\tbody:\n\t\treturn 0\nxxx\n"
),
AssemblyError::Syntax { .. }
));
}
#[test]
fn test_reject_declared_args_exceed_register_count()
{
let e = assert_rejects(
"Function(x@0, y@1) r#1 ⚅#0\n\textern[]\n\tbody:\n\t\treturn @0\n"
);
let AssemblyError::InsufficientRegisterCount {
register_count,
required,
..
} = e
else
{
panic!("expected InsufficientRegisterCount, got: {:?}", e);
};
assert_eq!(register_count, 1);
assert_eq!(required, 2);
}
#[test]
fn test_error_location_points_at_offending_line()
{
let e = assert_rejects(
"Function() r#1 ⚅#0\n\textern[]\n\tbody:\n\t\t@9 <- 1 + 2\n\t\treturn @9\n"
);
let location = e.location();
assert_eq!(
location.line, 4,
"expected line 4 (body instruction), got {}",
location.line
);
assert!(matches!(e, AssemblyError::RegisterOutOfBounds { .. }));
}
#[test]
fn test_display_renders_location_and_description()
{
let e = assert_rejects(
"Function() r#1 ⚅#0\n\textern[]\n\tbody:\n\t\t@5 <- 1 + 2\n\t\treturn @5\n"
);
let rendered = format!("{}", e);
assert!(
rendered.contains("line 4"),
"missing line number: {}",
rendered
);
assert!(
rendered.contains("register @5"),
"missing register token: {}",
rendered
);
assert!(
rendered.contains("exceeds declared register count r#1"),
"missing descriptive tail: {}",
rendered
);
}
#[test]
fn test_from_str_delegates_to_assembler()
{
let text = "\
Function() r#0 ⚅#0
\textern[]
\tbody:
\t\treturn 42
";
let function: Function = text.parse().unwrap();
assert_eq!(
function.instructions[0],
Instruction::r#return(AddressingMode::Immediate(Immediate(42)))
);
}
#[test]
fn test_from_str_propagates_errors()
{
let result: Result<Function, _> = "not a function".parse();
assert!(result.is_err());
}