valhallac

Compiler for set-theoretic programming language.
git clone git://git.knutsen.co/valhallac
Log | Files | Refs | README | LICENSE

commit 0d685187969bfcd3123055f1bee02ece4e490f5f
parent 85f7752a7fd2bb3fa60f3c294f86ccd4c8971209
Author: Demonstrandum <moi@knutsen.co>
Date:   Tue, 16 Jun 2020 02:32:42 +0100

Implemented basic type checking on function definitions.

Diffstat:
Msrc/bin.rs | 2--
Msrc/compiler/block.rs | 6+++---
Msrc/issue.rs | 12+++++++++---
Msrc/syntax/analysis/type_resolver.rs | 137++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/syntax/ast.rs | 28++++++++++++++++++++--------
Msrc/syntax/parser.rs | 19+++++++++++++++----
Msrc/syntax/token.rs | 2+-
Mtest_source.vh | 10+---------
Atests/expect_success/semi-colon.vh | 2++
Mtests/src/main.rs | 26++++++++++++++++----------
10 files changed, 192 insertions(+), 52 deletions(-)

diff --git a/src/bin.rs b/src/bin.rs @@ -179,8 +179,6 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> { } else { format!("{:0.5}s", seconds) }.white()); - - println!(); }); } diff --git a/src/compiler/block.rs b/src/compiler/block.rs @@ -166,7 +166,7 @@ impl<'a> LocalBlock<'a> { } fn function_assign(&mut self, left : &ast::CallNode, right : &'a Nodes) { - let mut arguments = left.collect_operands(); + let mut arguments = left.collect(); let base_node = arguments.remove(0); if let Nodes::Ident(ident) = base_node { @@ -412,11 +412,11 @@ impl<'a> fmt::Display for LocalBlock<'a> { for key in self.locals_map.keys() { writeln!(f, " | {: >3} | {}", self.locals_map[key], key)?; } - writeln!(f, " |====Globals=================")?; + writeln!(f, " |====Superiors===============")?; for (i, c) in self.globals.iter().enumerate() { writeln!(f, " | {: >3} | {}", i, c)?; } - writeln!(f, " |====Bytecodes===============")?; + writeln!(f, " |====Bytecode================")?; for inst in &self.instructions { if let Instr::Operand(_) = inst { write!(f, "{}", inst)?; diff --git a/src/issue.rs b/src/issue.rs @@ -73,13 +73,17 @@ impl Issue { } pub fn panic(&self) -> ! { - panic!("Cannot continue after such an issue.") + panic!("Cannot continue after such an issue: `{}'.", + self.message) } pub fn print(self) -> Self { unsafe { - crate::PANIC_MESSAGE = "Compilation could not continue."; - }; + #[cfg(feature="loud-panic")] + eprintln!("Issue was: `{}'.", self.message); + crate::PANIC_MESSAGE = + "Compilation could not continue."; + } eprintln!("\n{}", self); if self.is_fatal { @@ -155,6 +159,8 @@ impl fmt::Display for Issue { let mut line_content = if let Some(Ok(line_str)) = BufReader::new(file) .lines().nth(line - 1) { + // Add space at end to represent + // line-feed character. line_str + " " } else { "[**] THIS LINE DOES NOT EXIST! \ diff --git a/src/syntax/analysis/type_resolver.rs b/src/syntax/analysis/type_resolver.rs @@ -23,11 +23,13 @@ impl SymbolEntry { if let StaticTypes::TFunction(_, _) = self.signature { return true; } - return false; + false } - pub fn was_defined(&mut self) { + pub fn was_defined(&mut self) -> bool { + let def = self.defined; self.defined = true; + def } } @@ -132,6 +134,20 @@ fn search_chain(&mut self, ident : &str) -> Option<&mut SymbolTable> { return None; } +fn unwrap_set(&self, set : &StaticTypes) -> StaticTypes { + if let StaticTypes::TSet(internal) = set { + *internal.clone() + } else { + // We should never get here, we should always have + // checked earlier if a function signature tries to map + // between non-sets. + use crate::site::Site; + issue!(TypeError, Site::new().with_filename(&self.filename), + "Cannot create mapping (function) between non-sets.") + .crash_and_burn() + } +} + /// # Terminology /// `appl_0` - refers to the the 0th (base) application (call). /// `appl_n` - refers to any nested application n-levels deep. @@ -251,10 +267,10 @@ pub fn resolve_branch(&mut self, branch : &Nodes) -> Nodes { if maybe_op_inner_type.is_none() { // Fatal, we should really never get here, // because we _should_ check for this earlier. - issue!(TypeError, + fatal!(TypeError, (*appl_0.callee).site().with_filename(&self.filename), "Function should map from a set, it does not.") - .print(); + .print(); } // Safe to unwrap, we've checked for none. @@ -295,7 +311,7 @@ pub fn resolve_branch(&mut self, branch : &Nodes) -> Nodes { issue!(TypeError, appl_0.callee.site().with_filename(&self.filename), "Function-application / juxtaposition is not \ - defined on type of `{}'.", appl_0_st) + defined on type of `{}'.", appl_0_st) .print(); } } @@ -313,6 +329,19 @@ fn resolve_assignment(&mut self, // Either way, we must say it is now 'defined' // (as well as 'declared') in the table. + // Assignment with no type annotation needs to be + // more flexible when it comes to types, i.e. + // ``` + // a : Int + // a = 3 + // a = "Somethin" -- is illegal, doesn't match type. + // ``` + // Compared to: + // ``` + // a = 3 + // a = "Something" -- legal, `a' has type `Nat | String` now. + // ``` + // TODO: '=' with a type annotation should // cast the value on the right if possible, to // match the annotation. If not possible, throw @@ -322,6 +351,7 @@ fn resolve_assignment(&mut self, // a function (e.g. `f x = x + 1`). let filename = &self.filename.to_owned(); + let rhs = appl_0.operands[0].clone(); let lhs = &appl_1.operands[0]; // Handle variable (identifier) assignment: if let Nodes::Ident(ident_op_1) = lhs { @@ -381,19 +411,102 @@ fn resolve_assignment(&mut self, let base_call = call_op_1.base_call(); if !base_call.is_ident() { // Fatal, we must define the call on some sort of ident. - issue!(ParseError, + fatal!(ParseError, base_call.site().with_filename(&self.filename), "You have to assign to a call on an identifier, - this identifier is the function you are defining. - Expected an `identifier', found `{}'!", - base_call.node_type()) - .print(); + this identifier is the function you are defining.") + .note(&format!("Expected an `identifier', found `{}'!", + base_call.node_type())) + .print(); } // We've checked, and we may unwrap it. let base_call = base_call.ident().unwrap(); - // TODO Continue, collect the arguments too!!! + + let func_type; + if let Some(table) = self.search_chain(&base_call.value) { + let signatures = table.collect_signatures(&base_call.value); + let mut sig_iter = signatures.iter(); + // TODO: Select which signature to use (establish some order). + // Specifically need to select the correct overload. + // For now pick a random one. + func_type = sig_iter.next().unwrap().to_owned(); // We know this exists. + } else { + // TODO: Determine implicit type for this function. + // This has to be done in a way that considers the structure + // of the LHS, considering all pattern matches in each of the + // cases, and what their types are. The inductive case + // should also be analysed for what kind of functions it calls, + // in order to narrow down the type as much as possible. + func_type = StaticTypes::TUnknown; // FIXME. + } + + let mut left_type = StaticTypes::TUnknown; + let mut right_type = StaticTypes::TUnknown; + // Check if we do actually have a function type. + if let StaticTypes::TFunction(l, r) = func_type { + left_type = self.unwrap_set(&*l); // This should have already been + right_type = self.unwrap_set(&*r); // checked for. + } else { // Fatal, needs to be a function. + fatal!(TypeError, call_op_1.site.with_filename(&self.filename), + "Trying to define a function on a variable that does \ + not have type of `function'.") + .note(&format!("`{}' has type of `{}', which is not a function.", + base_call.value, func_type)) + .print(); + } + + let lhs_operands = call_op_1.collect_operands(); + let operand_count = lhs_operands.len(); + let mut function_scope = SymbolTable::new(&base_call.value); + for (i, lhs_operand) in lhs_operands.iter().enumerate() { + if let Nodes::Ident(lhs_op_ident) = lhs_operand { + function_scope.push(&lhs_op_ident.value, left_type.clone(), true); + if i == operand_count - 1 { + break; // No need to disect any further. + } + + if let StaticTypes::TFunction(l, r) = right_type { + left_type = self.unwrap_set(&*l); + right_type = self.unwrap_set(&*r); + } else { + fatal!(TypeError, + lhs_operands + .last().unwrap() + .site().with_filename(&self.filename), + "Function definition provided with too many arguments. + The type signature disagrees with the number + of arguments you have provided.") + .note("Consider removing this, or altering \ + the type signature.") + .print(); + } + } else { + // TODO: Not an ident, that means we're + // pattern matching. This will need a general + // implementation in the future. + } + } + // Now the function scope is populated with the arguments. + self.table_chain.push(function_scope); // Add the scope to the stack. + // Type the right side of the equality: + let typed_rhs = self.resolve_branch(&rhs); + // Check if the RHS has the correct type. + if typed_rhs.yield_type() == right_type { + appl_0.operands[0] = typed_rhs; + } else { + // TODO: If the the types disagree, but the type is + // a subset, just cast the type. For now, it's only an error: + issue!(TypeError, rhs.site().with_filename(&self.filename), + "Right hand side of function definition does not agree \ + with type signature. + Expected type of `{}', got `{}'.", + &right_type, &typed_rhs.yield_type()) + .note("Either convert the value, or alter the type signature.") + .print(); + } + // The function scope is no longer in use. + self.table_chain.pop(); } else { - // TODO: Assignment to functions. // TODO: Pattern matching etc. issue!(ParseError, diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs @@ -577,8 +577,9 @@ impl CallNode { } } - /// Collect arguments to a call. - pub fn collect_operands(&self) -> Vec<Nodes> { + + /// List of callee and operands in a list, in a double ended queue. + pub fn collect_deque(&self) -> VecDeque<Nodes> { fn make_argument_vector(call_node : &Nodes, operands : VecDeque<Nodes>) -> VecDeque<Nodes> { let mut pushable = operands; @@ -591,15 +592,26 @@ impl CallNode { pushable.push_front(call_node.clone()); return pushable; } - let q = make_argument_vector(&Nodes::Call(self.clone()), VecDeque::new()); - Vec::from(q) + + make_argument_vector( + &Nodes::Call(self.clone()), + VecDeque::new()) } - /// List of callee and operands, lisp call style list. + /// Flatten a function call into a list containing the + /// callee at the head, and arguments at tail (LISP style). + /// --- + /// Mainly for easier flattened/non-recursive + /// iteration over function calls. pub fn collect(&self) -> Vec<Nodes> { - let mut list = vec![self.base_call()]; - list.extend_from_slice(&self.collect_operands()); - list + Vec::from(self.collect_deque()) + } + + /// Collects only operands/arguments to a call. + pub fn collect_operands(&self) -> Vec<Nodes> { + let mut list = self.collect_deque(); + list.pop_front(); + Vec::from(list) } pub fn is_unary(&self) -> bool { diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs @@ -1,4 +1,5 @@ -use std::collections::VecDeque; +use std::collections::{VecDeque, HashSet}; +use lazy_static::lazy_static; use super::token; use super::ast; @@ -7,10 +8,19 @@ use super::operators; use crate::{issue, site}; use site::{Site, Location}; - use token::{Token, TokenType}; use ast::Nodes; +lazy_static! { + static ref EXPR_TERM : HashSet<TokenType> = { + let mut set = HashSet::new(); + set.insert(TokenType::RParen); + set.insert(TokenType::EOF); + set.insert(TokenType::Term); + set + }; +} + fn location_range(loc_begin : &Location, loc_end : &Location) -> Location { let mut loc_final = loc_end.clone(); @@ -204,13 +214,14 @@ impl<'a> ParseEnvironment<'a> { while self.optable.precedence(&self.stream[0].string).unwrap_or(190) > right_prec { - let next = &(&self.stream[0].string).clone(); + let ahead = self.stream[0].clone(); + let next = &ahead.string.clone(); if self.ignore_newline && next == "\n" { self.shift(); continue; } - if next == "\0" || next == "\n" || next == ")" { break; } + if EXPR_TERM.contains(&ahead.class) { break; } let maybe_op = self.optable.lookup(next, 2); if let Some(op) = maybe_op { diff --git a/src/syntax/token.rs b/src/syntax/token.rs @@ -18,7 +18,7 @@ enum Indent { /// Contains all possible types/classes of /// lexiacal tokens. -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Eq, Hash, Clone, Copy)] pub enum TokenType { /// Identifiers, variables, function names etc. Ident, diff --git a/test_source.vh b/test_source.vh @@ -2,24 +2,16 @@ -- TODO: Test casting from subsets of Real, upto Real. f : Real -> Real -> Real - f a b = (1 + 1) * a + b -- 2a + b a : Nat a = 3 -( : ) b Int +b : Int b = 1 - 8 -- -7 c : Real c = f (a + 1.0) (b - 0.0) ---* This should yield three errors *-- - -w : Beep -- Beep doesn't exits yet. --- This is an error, it should be `<-` not `:`. -w = a + b : Int - -2 = 3 -- Also an error. diff --git a/tests/expect_success/semi-colon.vh b/tests/expect_success/semi-colon.vh @@ -0,0 +1,2 @@ +3 + 2; 1 - 89; 2 - 3 * 4 +3 + 1 diff --git a/tests/src/main.rs b/tests/src/main.rs @@ -23,13 +23,16 @@ fn on_vh_files<F>(dir_path : &str, mut lambda : F) -> Result<Ratio, DynErr> where F : FnMut(&Path, String) -> bool { let (mut passes, mut total) = (0, 0); for entry in fs::read_dir(dir_path)? { - total += 1; if let Ok(path) = entry { let path = path.path(); if let Ok(source) = get_source(&path) { - if lambda(&path, source) { passes += 1 } + total += 1; + if lambda(&path, source) { passes += 1 } } else { // Otherwise just skip. - println!("Skipping `{}'...", path.to_string_lossy()); + println!(" Skipping `{}'...", path + .file_name().unwrap() + .to_string_lossy() + .underline()); } } } @@ -61,7 +64,9 @@ fn main() -> Result<(), DynErr> { let mut compile_attempt = |path: &Path, source: String| { count += 1; let filename = path.to_string_lossy(); - let prefix = format!("{: >4}. (`{}'):", + // For the log. + eprintln!(" === Compiling: `{}' ===", filename.underline()); + let prefix = format!("{: >3}. (`{}'):", count.to_string().bold(), path.file_stem().unwrap() .to_string_lossy() @@ -69,15 +74,16 @@ fn main() -> Result<(), DynErr> { .white()); // Catch errors: let did_panic = panic::catch_unwind(|| unsafe { + valhallac::PANIC_MESSAGE = ""; let tree = valhallac::parse_source(&source, &filename); if valhallac::PANIC_MESSAGE.is_empty() { // Try to compile. valhallac::compile(&tree); if !valhallac::PANIC_MESSAGE.is_empty() { - panic!("Did not pass."); + panic!("Did not pass: `{}'", valhallac::PANIC_MESSAGE); } } else { - panic!("Did not pass."); + panic!("Did not pass: `{}'", valhallac::PANIC_MESSAGE); } }); print!("{} {} ", prefix, ".".repeat(80 - prefix.len())); @@ -85,7 +91,7 @@ fn main() -> Result<(), DynErr> { }; // Expecting success: - println!("{} {}", "==>".blue().bold(), + println!("\n{} {}", "==>".blue().bold(), "Expecting compilation success:".white().bold()); let succ_ratio = on_vh_files("./expect_success", |path, source| { let passed = compile_attempt(path, source); @@ -98,9 +104,9 @@ fn main() -> Result<(), DynErr> { println!("{} {}", "==>".blue().bold(), "Expecting compilation failure:".white().bold()); let fail_ratio = on_vh_files("./expect_fail", |path, source| { - let passed = compile_attempt(path, source); - println!("{}", status(!passed)); - passed + let failed = !compile_attempt(path, source); + println!("{}", status(failed)); + failed })?; println!();