diff --git a/Cargo.lock b/Cargo.lock index bda183bb..1b47548b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -675,6 +675,7 @@ version = "0.1.0" dependencies = [ "glob", "prettyplease", + "proc-macro2", "quote", "syn 2.0.47", "thiserror", @@ -1786,9 +1787,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] diff --git a/fine/Cargo.lock b/fine/Cargo.lock index 89781166..8eb14daf 100644 --- a/fine/Cargo.lock +++ b/fine/Cargo.lock @@ -15,6 +15,7 @@ dependencies = [ "glob", "pretty_assertions", "prettyplease", + "proc-macro2", "quote", "syn", "thiserror", @@ -48,9 +49,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] diff --git a/fine/Cargo.toml b/fine/Cargo.toml index efe87168..d07b3794 100644 --- a/fine/Cargo.toml +++ b/fine/Cargo.toml @@ -9,6 +9,7 @@ pretty_assertions = "1.4.0" [build-dependencies] glob = "0.3.1" prettyplease = "0.2.16" +proc-macro2 = "1.0.76" quote = "1.0.35" syn = "2.0.47" diff --git a/fine/build.rs b/fine/build.rs index 5994df62..2cb81376 100644 --- a/fine/build.rs +++ b/fine/build.rs @@ -1,8 +1,25 @@ -use quote::{format_ident, quote}; +use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; +use quote::{format_ident, quote, TokenStreamExt}; use std::env; use std::fs; use std::path::{Path, PathBuf}; +struct ExpectedErrors(Vec); + +impl quote::ToTokens for ExpectedErrors { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut inner = TokenStream::new(); + for err in self.0.iter() { + inner.append(Literal::string(err)); + inner.append(Punct::new(',', Spacing::Alone)); + } + + tokens.append(Ident::new("vec", Span::call_site())); + tokens.append(Punct::new('!', Spacing::Joint)); + tokens.append(Group::new(Delimiter::Parenthesis, inner)) + } +} + fn generate_test_for_file(path: PathBuf) -> String { let contents = fs::read_to_string(&path) .expect("Unable to read input") @@ -92,6 +109,21 @@ fn generate_test_for_file(path: PathBuf) -> String { assertions.push(quote! { crate::assert_check_error(&_tree, &_lines, #expected); }); + } else if line == "@expect-errors:" { + let mut errors = Vec::new(); + while let Some(line) = lines.next() { + let line = match line.strip_prefix("// | ") { + Some(line) => line, + None => break, + }; + + errors.push(line.to_string()); + } + + let errors = ExpectedErrors(errors); + assertions.push(quote! { + crate::assert_errors(&_tree, &_lines, #errors); + }); } else if line.starts_with("@") { panic!("Test file {display_path} has unknown directive: {line}"); } diff --git a/fine/src/compiler.rs b/fine/src/compiler.rs index b3989d12..0c986125 100644 --- a/fine/src/compiler.rs +++ b/fine/src/compiler.rs @@ -14,6 +14,10 @@ pub enum Instruction { Panic, BoolNot, + Call(usize), + CompareBool, + CompareFloat, + CompareString, Discard, FloatAdd, FloatDivide, @@ -23,6 +27,8 @@ pub enum Instruction { JumpFalse(usize), JumpTrue(usize), LoadArgument(usize), + LoadExternFunction(usize), // NOTE: FUNKY, might want to indirect this index. + LoadFunction(usize), LoadLocal(usize), LoadModule(usize), PushFalse, @@ -30,16 +36,12 @@ pub enum Instruction { PushNothing, PushString(usize), PushTrue, + Return, + StoreArgument(usize), StoreLocal(usize), StoreModule(usize), - LoadFunction(usize), - LoadExternFunction(usize), // NOTE: FUNKY, might want to indirect this index. - Call(usize), - Return, StringAdd, - CompareBool, - CompareString, - CompareFloat, + Dup, } pub enum Export { @@ -372,30 +374,41 @@ fn compile_condition_expression(c: &mut Compiler, t: &Tree) -> CR { OK } -fn compile_binary_expression(c: &mut Compiler, t: TreeRef, tr: &Tree) -> CR { +fn compile_simple_binary_expression(c: &mut Compiler, tr: &Tree, f: T) -> CR +where + T: FnOnce(&mut Compiler, &Type) -> Instruction, +{ compile_expression(c, tr.nth_tree(0)?); + + let arg_tree = tr.nth_tree(2)?; + let arg_type = c.semantics.type_of(arg_tree); + + compile_expression(c, arg_tree); + + let inst = f(c, &arg_type); + c.push(inst); + + OK +} + +fn compile_binary_expression(c: &mut Compiler, t: TreeRef, tr: &Tree) -> CR { match tr.nth_token(1)?.kind { - TokenKind::Plus => { - compile_expression(c, tr.nth_tree(2)?); - c.push(match c.semantics.type_of(t) { - Type::F64 => Instruction::FloatAdd, - Type::String => Instruction::StringAdd, - _ => Instruction::Panic, - }); - } + TokenKind::Plus => compile_simple_binary_expression(c, tr, |_, t| match t { + Type::F64 => Instruction::FloatAdd, + Type::String => Instruction::StringAdd, + _ => Instruction::Panic, + }), TokenKind::Minus => { - compile_expression(c, tr.nth_tree(2)?); - c.push(Instruction::FloatSubtract); + compile_simple_binary_expression(c, tr, |_, _| Instruction::FloatSubtract) } TokenKind::Star => { - compile_expression(c, tr.nth_tree(2)?); - c.push(Instruction::FloatMultiply); + compile_simple_binary_expression(c, tr, |_, _| Instruction::FloatMultiply) } TokenKind::Slash => { - compile_expression(c, tr.nth_tree(2)?); - c.push(Instruction::FloatDivide); + compile_simple_binary_expression(c, tr, |_, _| Instruction::FloatDivide) } TokenKind::And => { + compile_expression(c, tr.nth_tree(0)?); let jump_false_index = c.push(Instruction::JumpFalse(0)); c.push(Instruction::PushTrue); @@ -406,8 +419,10 @@ fn compile_binary_expression(c: &mut Compiler, t: TreeRef, tr: &Tree) -> CR { compile_expression(c, tr.nth_tree(2)?); c.patch(jump_end_index, |i| Instruction::Jump(i)); + OK } TokenKind::Or => { + compile_expression(c, tr.nth_tree(0)?); let jump_true_index = c.push(Instruction::JumpTrue(0)); c.push(Instruction::PushTrue); @@ -418,29 +433,70 @@ fn compile_binary_expression(c: &mut Compiler, t: TreeRef, tr: &Tree) -> CR { compile_expression(c, tr.nth_tree(2)?); c.patch(jump_end_index, |i| Instruction::Jump(i)); + OK } TokenKind::EqualEqual => { - let arg_tree = tr.nth_tree(2)?; - let arg_type = c.semantics.type_of(arg_tree); - + compile_simple_binary_expression(c, tr, |c, arg_type| { + if c.semantics.type_compat(&arg_type, &Type::Nothing) { + c.push(Instruction::Discard); + c.push(Instruction::Discard); + Instruction::PushTrue + } else { + match arg_type { + Type::F64 => Instruction::CompareFloat, + Type::String => Instruction::CompareString, + Type::Bool => Instruction::CompareBool, // ? + _ => Instruction::Panic, + } + } + }) + } + TokenKind::Equal => { compile_expression(c, tr.nth_tree(2)?); + c.push(Instruction::Dup); - if c.semantics.type_compat(&arg_type, &Type::Nothing) { - c.push(Instruction::Discard); - c.push(Instruction::Discard); - c.push(Instruction::PushTrue); - } else { - c.push(match arg_type { - Type::F64 => Instruction::CompareFloat, - Type::String => Instruction::CompareString, - Type::Bool => Instruction::CompareBool, // ? - _ => Instruction::Panic, - }); + let lvalue = tr.nth_tree(0)?; + let ltree = &c.syntax[lvalue]; + match ltree.kind { + TreeKind::Identifier => { + let ident = ltree.nth_token(0)?; + let environment = c.semantics.environment_of(lvalue); + let declaration = environment.bind(ident)?; + + let instruction = match declaration { + Declaration::Variable { + location, index, .. + } => { + let index = *index; + match location { + Location::Argument => { + compiler_assert!(c, t, index < c.function.args); + Instruction::StoreArgument(index) + } + Location::Local => { + if index >= c.function.locals { + c.function.locals = index + 1; + } + Instruction::StoreLocal(index) + } + Location::Module => { + compiler_assert!(c, t, index < c.module.globals); + Instruction::StoreModule(index) + } + } + } + + Declaration::ExternFunction { .. } => Instruction::Panic, + Declaration::Function { .. } => Instruction::Panic, + }; + c.push(instruction); + } + _ => ice!(c, t, "Unsupported lvalue type"), } + OK } _ => ice!(c, t, "Unsupported binary expression"), } - OK } fn compile_identifier_expression(c: &mut Compiler, t: TreeRef, tree: &Tree) -> Option<()> { diff --git a/fine/src/parser.rs b/fine/src/parser.rs index 9973cf44..68bdf198 100644 --- a/fine/src/parser.rs +++ b/fine/src/parser.rs @@ -119,30 +119,32 @@ impl<'a> std::ops::IndexMut for SyntaxTree<'a> { #[derive(Debug, Eq, PartialEq)] pub enum TreeKind { Error, - File, - FunctionDecl, - ParamList, - Parameter, - TypeExpression, - Block, - LetStatement, - ReturnStatement, - ExpressionStatement, - LiteralExpression, - GroupingExpression, - UnaryExpression, - ConditionalExpression, - CallExpression, - ArgumentList, + Argument, + ArgumentList, BinaryExpression, - IfStatement, + Block, + CallExpression, + ConditionalExpression, + ExpressionStatement, + File, + ForStatement, + FunctionDecl, + GroupingExpression, Identifier, - ReturnType, - TypeParameterList, - TypeParameter, + IfStatement, + LetStatement, ListConstructor, ListConstructorElement, + LiteralExpression, + ParamList, + Parameter, + ReturnStatement, + ReturnType, + TypeExpression, + TypeParameter, + TypeParameterList, + UnaryExpression, } pub struct Tree<'a> { @@ -628,6 +630,7 @@ fn statement(p: &mut CParser) { TokenKind::LeftBrace => block(p), TokenKind::Let => statement_let(p), TokenKind::Return => statement_return(p), + TokenKind::For => statement_for(p), // NOTE: Technically 'if' is an expression, but `if` doesn't // require a semicolon at the end if it's all by itself. @@ -677,6 +680,22 @@ fn statement_return(p: &mut CParser) { p.end(m, TreeKind::ReturnStatement); } +fn statement_for(p: &mut CParser) { + assert!(p.at(TokenKind::For)); + let m = p.start(); + + p.expect(TokenKind::For, "expect a for to start a for loop"); + p.expect( + TokenKind::Identifier, + "expected an identifier for the loop variable", + ); + p.expect(TokenKind::In, "expect an 'in' after the loop variable"); + expression(p); + block(p); + + p.end(m, TreeKind::ForStatement); +} + fn statement_expression(p: &mut CParser) { let m = p.start(); diff --git a/fine/src/semantics.rs b/fine/src/semantics.rs index 0b92e35f..598ebcdc 100644 --- a/fine/src/semantics.rs +++ b/fine/src/semantics.rs @@ -69,6 +69,12 @@ pub enum Type { // everything's fine. Unreachable, + // The type of an assignment expression. Assignments look like + // expressions but cannot be used in places that expect them (e.g., in + // `if` conditions), and so this is how we signal that. (We can, however, + // chain assignments, and so we flow the type of the assignment through.) + Assignment(Box), + // This is until generics are working MagicPrintGarbage, @@ -108,6 +114,7 @@ impl fmt::Display for Type { match self { Error => write!(f, "<< INTERNAL ERROR >>"), Unreachable => write!(f, "<< UNREACHABLE >>"), + Assignment(_) => write!(f, "assignment"), Nothing => write!(f, "()"), F64 => write!(f, "f64"), String => write!(f, "string"), @@ -138,6 +145,8 @@ pub enum Location { Argument, Local, Module, + // TODO: Member + // TODO: ArrayIndex } // TODO: Is `usize` what we want? Do we want e.g. dyn trait for invoke? @@ -300,6 +309,24 @@ fn set_logical_parents( } } } + TreeKind::ForStatement => { + let body = tree.child_of_kind(syntax_tree, TreeKind::Block); + for child in &tree.children { + match child { + Child::Token(_) => (), + Child::Tree(ct) => { + if Some(*ct) == body { + set_logical_parents(parents, syntax_tree, *ct, Some(t)); + } else { + // If it's not the body then it must be the + // iterable and the iterable doesn't have the + // loop variable in scope. + set_logical_parents(parents, syntax_tree, *ct, parent); + } + } + } + } + } _ => { // By default, the parent for each child is current tree. for child in &tree.children { @@ -424,6 +451,9 @@ impl<'a> Semantics<'a> { self.report_error_span(tree.start_pos, tree.end_pos, error) } + // pub fn lvalue_declaration(&self, t: TreeRef) -> Option<&Declaration> { + // } + fn gather_errors(&mut self, tree: TreeRef) { let mut stack = vec![tree]; while let Some(tr) = stack.pop() { @@ -475,6 +505,8 @@ impl<'a> Semantics<'a> { TreeKind::File => self.environment_of_file(parent, tree), TreeKind::Block => self.environment_of_block(parent, tree), + TreeKind::ForStatement => self.environment_of_for(parent, tree), + // TODO: Blocks should introduce a local environment if required. // Test with a type error in a block statement and a // binding outside. You will need a new assertion type and @@ -606,7 +638,31 @@ impl<'a> Semantics<'a> { EnvironmentRef::new(environment) } + fn environment_of_for(&self, parent: EnvironmentRef, tree: &Tree) -> EnvironmentRef { + let Some(id) = tree.nth_token(1) else { + return parent; + }; + + let Some(enumerable) = tree.nth_tree(3) else { + return parent; + }; + + let item_type = match self.type_of(enumerable) { + Type::Error => Type::Error, + Type::List(x) => (&*x).clone(), + _ => { + self.report_error_tree_ref(enumerable, "this expression is not enumerable"); + Type::Error + } + }; + + let mut environment = Environment::new(Some(parent), Location::Local); + environment.insert(id, item_type); + EnvironmentRef::new(environment) + } + pub fn type_compat(&self, a: &Type, b: &Type) -> bool { + // TODO: Convert this into "can become" or something. // TODO: This is wrong; we because of numeric literals etc. match (a, b) { (Type::F64, Type::F64) => true, @@ -615,12 +671,20 @@ impl<'a> Semantics<'a> { (Type::Unreachable, Type::Unreachable) => true, (Type::Nothing, Type::Nothing) => true, + (Type::List(a), Type::List(b)) => self.type_compat(a, b), + (Type::Function(a_args, a_ret), Type::Function(b_args, b_ret)) => { + a_args.len() == b_args.len() + && self.type_compat(a_ret, b_ret) + && a_args + .iter() + .zip(b_args.iter()) + .all(|(a, b)| self.type_compat(a, b)) + } + // Avoid introducing more errors (Type::Error, _) => true, (_, Type::Error) => true, - (Type::List(a), Type::List(b)) => self.type_compat(a, b), - // TODO: Unification on type variables! :D (_, _) => false, } @@ -660,6 +724,7 @@ impl<'a> Semantics<'a> { TreeKind::LetStatement => Some(Type::Nothing), TreeKind::ReturnStatement => Some(Type::Unreachable), + TreeKind::ForStatement => Some(Type::Nothing), TreeKind::ExpressionStatement => self.type_of_expression_statement(tree), TreeKind::IfStatement => self.type_of_if_statement(tree), TreeKind::Identifier => self.type_of_identifier(t, tree), @@ -720,7 +785,8 @@ impl<'a> Semantics<'a> { fn type_of_binary(&self, tree: &Tree) -> Option { assert_eq!(tree.kind, TreeKind::BinaryExpression); - let lhs = self.type_of(tree.nth_tree(0)?); + let left_tree = tree.nth_tree(0)?; + let lhs = self.type_of(left_tree); let op = tree.nth_token(1)?; let rhs = self.type_of(tree.nth_tree(2)?); @@ -760,6 +826,9 @@ impl<'a> Semantics<'a> { (_, Type::Error, _) => Some(Type::Error), (_, _, Type::Error) => Some(Type::Error), + // Assignments are fun. + (TokenKind::Equal, a, b) => self.type_of_assignment(left_tree, a, b, op), + // Missed the whole table, it must be an error. (_, left_type, right_type) => { self.report_error( @@ -771,6 +840,63 @@ impl<'a> Semantics<'a> { } } + fn type_of_assignment( + &self, + left_tree: TreeRef, + left_type: Type, + right_type: Type, + op: &Token, + ) -> Option { + // Ensure the left tree is an lvalue + let environment = self.environment_of(left_tree); + let tree = &self.syntax_tree[left_tree]; + let declaration = match tree.kind { + // TODO: Assign to member access or list access + TreeKind::Identifier => environment.bind(tree.nth_token(0)?), + _ => None, + }; + match declaration { + Some(d) => match d { + Declaration::Variable { .. } => (), + Declaration::ExternFunction { .. } | Declaration::Function { .. } => { + self.report_error_tree_ref( + left_tree, + "cannot assign a new value to a function declaration", + ); + return Some(Type::Error); + } + }, + None => { + self.report_error_tree_ref( + left_tree, + "cannot assign a value to this expression, it is not a place you can store things", + ); + return Some(Type::Error); + } + } + + let left_type = match left_type { + Type::Assignment(x) => *x, + t => t, + }; + let right_type = match right_type { + Type::Assignment(x) => *x, + t => t, + }; + + if left_type.is_error() || right_type.is_error() { + Some(Type::Error) + } else if self.type_compat(&left_type, &right_type) { + Some(Type::Assignment(Box::new(left_type))) + } else { + self.report_error( + op.start, + format!("cannot assign a value of type `{right_type}` to type `{left_type}`"), + ); + Some(Type::Error) + } + } + fn type_of_type_expr(&self, tree: &Tree) -> Option { assert_eq!(tree.kind, TreeKind::TypeExpression); @@ -1216,6 +1342,7 @@ pub fn check(s: &Semantics) { TreeKind::ListConstructorElement => { let _ = s.type_of(t); } + TreeKind::ForStatement => check_for_statement(s, t), } } } @@ -1295,6 +1422,10 @@ fn check_return_statement(s: &Semantics, tree: &Tree) { // OK this one is a little bit messed up because it reaches *up*, sorry. } +fn check_for_statement(s: &Semantics, t: TreeRef) { + let _ = s.environment_of(t); +} + #[cfg(test)] mod tests { use super::*; diff --git a/fine/src/tokens.rs b/fine/src/tokens.rs index db211457..707b65d9 100644 --- a/fine/src/tokens.rs +++ b/fine/src/tokens.rs @@ -47,6 +47,7 @@ pub enum TokenKind { Fun, If, Import, + In, Let, Or, Return, @@ -292,6 +293,9 @@ impl<'a> Tokens<'a> { if ident == "import" { return TokenKind::Import; } + if ident == "in" { + return TokenKind::In; + } } 'l' => { if ident == "let" { diff --git a/fine/src/vm.rs b/fine/src/vm.rs index 9fa42424..68d05a2c 100644 --- a/fine/src/vm.rs +++ b/fine/src/vm.rs @@ -161,6 +161,15 @@ impl Frame { } } + fn store_argument(&mut self, i: usize, v: StackValue) -> Result<()> { + if i >= self.locals.len() { + Err(VMErrorCode::LocalOutOfRange(i)) + } else { + self.args[i] = v; + Ok(()) + } + } + fn pop_function(&mut self) -> Result { match self.pop_value()? { StackValue::Function(f) => Ok(FuncValue::Function(f)), @@ -236,6 +245,11 @@ fn eval_one( Instruction::Discard => { f.pop_value()?; } + Instruction::Dup => { + let v = f.pop_value()?; + f.push_value(v.clone()); + f.push_value(v); + } Instruction::FloatAdd => { let x = f.pop_float()?; let y = f.pop_float()?; @@ -300,6 +314,10 @@ fn eval_one( Instruction::PushTrue => { f.push_bool(true); } + Instruction::StoreArgument(i) => { + let v = f.pop_value()?; + f.store_argument(i, v)?; + } Instruction::StoreLocal(i) => { let v = f.pop_value()?; f.store_local(i, v)?; diff --git a/fine/tests/example_tests.rs b/fine/tests/example_tests.rs index 5c5230a2..9dd6f7e4 100644 --- a/fine/tests/example_tests.rs +++ b/fine/tests/example_tests.rs @@ -283,6 +283,25 @@ fn assert_eval_ok(tree: &SyntaxTree, lines: &Lines, expected: &str) { } } +fn assert_errors(tree: &SyntaxTree, lines: &Lines, expected_errors: Vec<&str>) { + let semantics = Semantics::new(tree, lines); + check(&semantics); + + let errors: Vec = semantics + .snapshot_errors() + .iter() + .map(|e| format!("{}", e)) + .collect(); + + semantic_assert_eq!( + &semantics, + None, + expected_errors, + errors, + "expected no errors" + ); +} + fn assert_check_error(tree: &SyntaxTree, lines: &Lines, expected: &str) { let semantics = Semantics::new(tree, lines); check(&semantics); diff --git a/fine/tests/expression/assignment.fine b/fine/tests/expression/assignment.fine new file mode 100644 index 00000000..56497b75 --- /dev/null +++ b/fine/tests/expression/assignment.fine @@ -0,0 +1,34 @@ +fun test() -> f64 { + let x = 12; + let y = 13; + let z = 2; + x = y = z; + x +} + +// @no-errors +// @compiles-to: +// | function << module >> (0 args, 0 locals): +// | strings (0): +// | code (2): +// | 0: PushNothing +// | 1: Return +// | function test (0 args, 3 locals): +// | strings (0): +// | code (14): +// | 0: PushFloat(12.0) +// | 1: StoreLocal(0) +// | 2: PushFloat(13.0) +// | 3: StoreLocal(1) +// | 4: PushFloat(2.0) +// | 5: StoreLocal(2) +// | 6: LoadLocal(2) +// | 7: Dup +// | 8: StoreLocal(1) +// | 9: Dup +// | 10: StoreLocal(0) +// | 11: Discard +// | 12: LoadLocal(0) +// | 13: Return +// | +// @eval: Float(2.0) diff --git a/fine/tests/expression/errors/assignment_errors.fine b/fine/tests/expression/errors/assignment_errors.fine new file mode 100644 index 00000000..cf106dd3 --- /dev/null +++ b/fine/tests/expression/errors/assignment_errors.fine @@ -0,0 +1,20 @@ +fun something() { } +fun something_else() { } + +fun wrong() { + let x = 12; + let y = "hello"; + x = y; // should be an error + y = x; // should be an error + + let z = 23; + y = x = z; // should be an error + + something_else = something; // should be an error +} + +// @expect-errors: +// | 7:4: cannot assign a value of type `string` to type `f64` +// | 8:4: cannot assign a value of type `f64` to type `string` +// | 11:4: cannot assign a value of type `f64` to type `string` +// | 13:2: cannot assign a new value to a function declaration