From d8988cb2cf6568deff7bfe72018baa160b5bd463 Mon Sep 17 00:00:00 2001 From: John Doty Date: Thu, 11 Jan 2024 06:33:08 -0800 Subject: [PATCH] [fine] test compilation, start removing print --- fine/build.rs | 15 +++++ fine/src/compiler.rs | 93 +++++++++++++++++++++++--- fine/src/parser.rs | 23 ------- fine/src/tokens.rs | 21 ++---- fine/tests/example_tests.rs | 94 ++++++++++++++++++++++----- fine/tests/expression/argument.fine | 22 +++++-- fine/tests/expression/arithmetic.fine | 23 +++++-- fine/tests/expression/variable.fine | 29 ++++++--- 8 files changed, 239 insertions(+), 81 deletions(-) diff --git a/fine/build.rs b/fine/build.rs index 97a76612..0b92b92b 100644 --- a/fine/build.rs +++ b/fine/build.rs @@ -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() diff --git a/fine/src/compiler.rs b/fine/src/compiler.rs index a187aa00..e4033581 100644 --- a/fine/src/compiler.rs +++ b/fine/src/compiler.rs @@ -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, @@ -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, 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 } diff --git a/fine/src/parser.rs b/fine/src/parser.rs index 8f71fbe7..13cdd620 100644 --- a/fine/src/parser.rs +++ b/fine/src/parser.rs @@ -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(); diff --git a/fine/src/tokens.rs b/fine/src/tokens.rs index c634d7c3..3c7409bb 100644 --- a/fine/src/tokens.rs +++ b/fine/src/tokens.rs @@ -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!( diff --git a/fine/tests/example_tests.rs b/fine/tests/example_tests.rs index f25af7ed..cd0445c3 100644 --- a/fine/tests/example_tests.rs +++ b/fine/tests/example_tests.rs @@ -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")); diff --git a/fine/tests/expression/argument.fine b/fine/tests/expression/argument.fine index bf9de0b8..614aba4d 100644 --- a/fine/tests/expression/argument.fine +++ b/fine/tests/expression/argument.fine @@ -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): +// | diff --git a/fine/tests/expression/arithmetic.fine b/fine/tests/expression/arithmetic.fine index 610838d1..3323f33c 100644 --- a/fine/tests/expression/arithmetic.fine +++ b/fine/tests/expression/arithmetic.fine @@ -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 \ No newline at end of file +// | +// @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 +// | diff --git a/fine/tests/expression/variable.fine b/fine/tests/expression/variable.fine index f1b002e8..0e23bc5c 100644 --- a/fine/tests/expression/variable.fine +++ b/fine/tests/expression/variable.fine @@ -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 +// |