[fine] test compilation, start removing print

This commit is contained in:
John Doty 2024-01-11 06:33:08 -08:00
parent d8db65af55
commit d8988cb2cf
8 changed files with 239 additions and 81 deletions

View file

@ -37,6 +37,21 @@ fn generate_test_for_file(path: PathBuf) -> String {
assertions.push(quote! {
crate::assert_concrete(&_tree, #concrete, #display_path);
});
} else if line == "@compiles-to:" {
let mut compiled = String::new();
while let Some(line) = lines.next() {
let line = match line.strip_prefix("// | ") {
Some(line) => line,
None => break,
};
compiled.push_str(line);
compiled.push_str("\n");
}
assertions.push(quote! {
crate::assert_compiles_to(&_tree, &_lines, #compiled, #display_path);
});
} else if let Some(line) = line.strip_prefix("@type:") {
let (pos, expected) = line
.trim()

View file

@ -6,8 +6,34 @@ use crate::{
tokens::TokenKind,
};
macro_rules! compiler_assert_eq {
($compiler:expr, $tr:expr, $ll:expr, $rr:expr, $($t:tt)*) => {{
let left = &$ll;
let right = &$rr;
if left != right {
let semantics = $compiler.semantics;
semantics.dump_compiler_state(Some($tr));
let message = format!($($t)*);
assert_eq!(left, right, "{}", message);
}
}};
}
macro_rules! compiler_assert {
($compiler:expr, $tr:expr, $($t:tt)*) => {{
if !($($t)*) {
let semantics = $compiler.semantics;
semantics.dump_compiler_state(Some($tr));
assert!($($t)*);
}
}};
}
// TODO: If I were cool this would by actual bytecode.
// But I'm not cool.
#[derive(Debug)]
pub enum Instruction {
Panic,
@ -53,8 +79,13 @@ impl Module {
init: 0,
}
}
pub fn functions(&self) -> &[Function] {
&self.functions
}
}
// TODO: Debug information.
pub struct Function {
name: String,
instructions: Vec<Instruction>,
@ -64,12 +95,12 @@ pub struct Function {
}
impl Function {
pub fn new(name: &str) -> Self {
pub fn new(name: &str, args: usize) -> Self {
Function {
name: name.to_string(),
instructions: Vec::new(),
strings: Vec::new(),
args: 0,
args,
locals: 0,
}
}
@ -77,12 +108,29 @@ impl Function {
pub fn name(&self) -> &str {
&self.name
}
pub fn args(&self) -> usize {
self.args
}
pub fn locals(&self) -> usize {
self.locals
}
pub fn strings(&self) -> &[String] {
&self.strings
}
pub fn instructions(&self) -> &[Instruction] {
&self.instructions
}
}
struct Compiler<'a> {
semantics: &'a Semantics<'a>,
syntax: &'a SyntaxTree<'a>,
function_bindings: HashMap<String, usize>,
module: Module,
function: Function,
}
@ -114,8 +162,9 @@ pub fn compile(semantics: &Semantics) -> Module {
let mut compiler = Compiler {
semantics,
syntax: semantics.tree(),
function_bindings: HashMap::new(),
module: Module::new(),
function: Function::new("<< module >>"),
function: Function::new("<< module >>", 0),
};
if let Some(t) = semantics.tree().root() {
@ -132,7 +181,7 @@ pub fn compile(semantics: &Semantics) -> Module {
fn file(c: &mut Compiler, t: TreeRef) {
let tree = &c.syntax[t];
assert_eq!(tree.kind, TreeKind::File);
compiler_assert_eq!(c, t, tree.kind, TreeKind::File, "must be compiling a file");
for i in 0..tree.children.len() {
if let Some(t) = tree.nth_tree(i) {
compile_statement(c, t, false);
@ -306,11 +355,11 @@ fn compile_identifier_expression(c: &mut Compiler, t: TreeRef, tree: &Tree) -> O
Instruction::LoadLocal(declaration.index)
}
Location::Argument => {
assert!(declaration.index < c.function.args);
compiler_assert!(c, t, declaration.index < c.function.args);
Instruction::LoadArgument(declaration.index)
}
Location::Module => {
assert!(declaration.index < c.module.globals);
compiler_assert!(c, t, declaration.index < c.module.globals);
Instruction::LoadModule(declaration.index)
}
};
@ -398,6 +447,34 @@ fn compile_let_statement(c: &mut Compiler, t: TreeRef, tree: &Tree, gen_value: b
OK
}
fn compile_function_declaration(_c: &mut Compiler, _tree: &Tree, _gen_value: bool) -> CR {
todo!()
fn compile_function_declaration(c: &mut Compiler, tree: &Tree, gen_value: bool) -> CR {
let name = tree.nth_token(1)?;
let block = if tree
.nth_token(3)
.is_some_and(|t| t.kind == TokenKind::Arrow)
{
tree.nth_tree(4)?
} else {
tree.nth_tree(3)?
};
let arg_list = tree.nth_tree(2)?;
let arg_count = c.syntax[arg_list].children.len() - 2;
let mut prev = Function::new(name.as_str(), arg_count);
std::mem::swap(&mut c.function, &mut prev);
c.function_bindings
.insert(c.function.name.clone(), c.module.functions.len());
compile_expression(c, block);
std::mem::swap(&mut c.function, &mut prev);
c.module.functions.push(prev);
if gen_value {
c.push(Instruction::PushNothing);
}
OK
}

View file

@ -138,7 +138,6 @@ pub enum TreeKind {
BinaryExpression,
IfStatement,
Identifier,
PrintStatement,
}
pub struct Tree<'a> {
@ -556,32 +555,10 @@ fn statement(p: &mut CParser) {
// require a semicolon at the end if it's all by itself.
TokenKind::If => statement_if(p),
TokenKind::Print => statement_print(p),
_ => statement_expression(p),
}
}
fn statement_print(p: &mut CParser) {
assert!(p.at(TokenKind::Print));
let m = p.start();
p.expect(
TokenKind::Print,
"expect 'print' to start a print statement",
);
p.expect(TokenKind::LeftParen, "expect '(' to start a print");
if !p.at(TokenKind::RightParen) {
expression(p);
}
p.expect(TokenKind::RightParen, "expect ')' after a print statement");
if !p.at(TokenKind::RightBrace) {
p.expect(TokenKind::Semicolon, "expect ';' to end a print statement");
}
p.end(m, TreeKind::PrintStatement);
}
fn statement_if(p: &mut CParser) {
assert!(p.at(TokenKind::If));
let m = p.start();

View file

@ -49,7 +49,6 @@ pub enum TokenKind {
Import,
Let,
Or,
Print,
Return,
Select,
This,
@ -304,11 +303,6 @@ impl<'a> Tokens<'a> {
return TokenKind::Or;
}
}
'p' => {
if ident == "print" {
return TokenKind::Print;
}
}
'r' => {
if ident == "return" {
return TokenKind::Return;
@ -575,18 +569,17 @@ mod tests {
test_tokens!(
more_keywords,
"fun if import let print return select this true while truewhile",
"fun if import let return select this true while truewhile",
(0, Fun, "fun"),
(4, If, "if"),
(7, Import, "import"),
(14, Let, "let"),
(18, Print, "print"),
(24, Return, "return"),
(31, Select, "select"),
(38, This, "this"),
(43, True, "true"),
(48, While, "while"),
(54, Identifier, "truewhile")
(18, Return, "return"),
(25, Select, "select"),
(32, This, "this"),
(37, True, "true"),
(42, While, "while"),
(48, Identifier, "truewhile")
);
test_tokens!(

View file

@ -1,30 +1,32 @@
use fine::compiler::{compile, Function, Module};
use fine::parser::SyntaxTree;
use fine::semantics::{Semantics, Type};
use fine::tokens::Lines;
use pretty_assertions::assert_eq;
use std::fmt::Write as _;
fn rebase_concrete(source_path: &str, dump: &str) {
fn rebase_section(source_path: &str, section: &str, value: &str) {
let contents = std::fs::read_to_string(source_path)
.expect(&format!("unable to read input file {}", source_path));
let mut result = String::new();
let mut lines = contents.lines();
// Search for the "concrete:" section.
let mut found_concrete_section = false;
// Search for the section.
let mut found_section = false;
let marker = format!("// @{section}:");
while let Some(line) = lines.next() {
result.push_str(line);
result.push_str("\n");
if line == "// @concrete:" {
found_concrete_section = true;
if line == marker {
found_section = true;
break;
}
}
if !found_concrete_section {
if !found_section {
panic!(
"unable to locate the concrete section in {}. Is there a line that starts with '// concrete:'?",
source_path
"unable to locate the {section} section in {source_path}. Is there a line that starts with '// @{section}:'?"
);
}
@ -39,7 +41,7 @@ fn rebase_concrete(source_path: &str, dump: &str) {
// OK we're out of concrete syntax tree; copy in the
// new CST. (We do this inline so we don't lose
// `line`.)
for expected_line in dump.lines() {
for expected_line in value.lines() {
result.push_str("// | ");
result.push_str(expected_line);
result.push_str("\n");
@ -70,18 +72,24 @@ fn rebase_concrete(source_path: &str, dump: &str) {
std::fs::write(source_path, result).expect("unable to write the new file!");
}
fn assert_concrete(tree: &SyntaxTree, expected: &str, source_path: &str) {
let dump = tree.dump(false);
fn should_rebase() -> bool {
let rebase = std::env::var("FINE_TEST_REBASE")
.unwrap_or(String::new())
.to_lowercase();
match rebase.as_str() {
"1" | "true" | "yes" | "y" => {
if dump != expected {
rebase_concrete(source_path, &dump)
}
"1" | "true" | "yes" | "y" => true,
_ => false,
}
}
fn assert_concrete(tree: &SyntaxTree, expected: &str, source_path: &str) {
let dump = tree.dump(false);
if dump != expected {
if should_rebase() {
rebase_section(source_path, "concrete", &dump)
} else {
assert_eq!(expected, dump, "concrete syntax trees did not match (set FINE_TEST_REBASE=1 to auto-rebase if the diff is expected)")
}
_ => assert_eq!(expected, dump, "concrete syntax trees did not match (set FINE_TEST_REBASE=1 to auto-rebase if the diff is expected)"),
}
}
@ -181,4 +189,58 @@ fn assert_type_error_at(
);
}
fn dump_function(out: &mut String, function: &Function) -> std::fmt::Result {
writeln!(
out,
"function {} ({} args, {} locals):",
function.name(),
function.args(),
function.locals()
)?;
let strings = function.strings();
writeln!(out, " strings ({}):", strings.len())?;
for (i, s) in strings.iter().enumerate() {
writeln!(out, " {}: {}", i, s)?; // TODO: ESCAPE
}
let code = function.instructions();
writeln!(out, " code ({}):", code.len())?;
for (i, inst) in code.iter().enumerate() {
writeln!(out, " {}: {:?}", i, inst)?;
}
Ok(())
}
fn dump_module(out: &mut String, module: &Module) -> std::fmt::Result {
for function in module.functions() {
dump_function(out, function)?;
}
Ok(())
}
fn assert_compiles_to(tree: &SyntaxTree, lines: &Lines, expected: &str, source_path: &str) {
let semantics = Semantics::new(tree, lines);
let module = compile(&semantics);
let mut actual = String::new();
dump_module(&mut actual, &module).expect("no dumping?");
if expected != actual {
if should_rebase() {
rebase_section(source_path, "compiles-to", &actual)
} else {
semantic_assert_eq!(
&semantics,
None,
expected,
actual,
"did not compile as expected (set FINE_TEST_REBASE=1 to auto-rebase if the diff is expected)"
)
}
}
}
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));

View file

@ -1,3 +1,8 @@
fun foo(x: f64) {
x + 7
}
// @type: 20 f64
// @concrete:
// | File
// | FunctionDecl
@ -22,9 +27,14 @@
// | Number:'"7"'
// | RightBrace:'"}"'
// |
fun foo(x: f64) {
x + 7
}
// @type: 613 f64
// @compiles-to:
// | function foo (1 args, 0 locals):
// | strings (0):
// | code (3):
// | 0: LoadArgument(0)
// | 1: PushFloat(7.0)
// | 2: FloatAdd
// | function << module >> (0 args, 0 locals):
// | strings (0):
// | code (0):
// |

View file

@ -1,3 +1,6 @@
1 * 2 + -3 * 4;
// @type: 6 f64
// @concrete:
// | File
// | ExpressionStatement
@ -18,7 +21,19 @@
// | LiteralExpression
// | Number:'"4"'
// | Semicolon:'";"'
//
1 * 2 + -3 * 4;
// @type: 532 f64
// |
// @compiles-to:
// | function << module >> (0 args, 0 locals):
// | strings (0):
// | code (10):
// | 0: PushFloat(1.0)
// | 1: PushFloat(2.0)
// | 2: FloatMultiply
// | 3: PushFloat(3.0)
// | 4: PushFloat(-1.0)
// | 5: FloatMultiply
// | 6: PushFloat(4.0)
// | 7: FloatMultiply
// | 8: FloatAdd
// | 9: Discard
// |

View file

@ -1,3 +1,8 @@
let x = 23;
let y = x * 2;
y;
// @type: 27 f64
// @concrete:
// | File
// | LetStatement
@ -18,17 +23,21 @@
// | LiteralExpression
// | Number:'"2"'
// | Semicolon:'";"'
// | PrintStatement
// | Print:'"print"'
// | LeftParen:'"("'
// | ExpressionStatement
// | Identifier
// | Identifier:'"y"'
// | RightParen:'")"'
// | Semicolon:'";"'
// |
let x = 23;
let y = x * 2;
print(y);
// @type: 667 f64
// @compiles-to:
// | function << module >> (0 args, 0 locals):
// | strings (0):
// | code (8):
// | 0: PushFloat(23.0)
// | 1: StoreModule(0)
// | 2: LoadModule(0)
// | 3: PushFloat(2.0)
// | 4: FloatMultiply
// | 5: StoreModule(1)
// | 6: LoadModule(1)
// | 7: Discard
// |