From 9e3a849f9f072e7dc39b95ae06839682f84cbc40 Mon Sep 17 00:00:00 2001 From: grisenti Date: Sat, 18 May 2024 08:49:41 +0200 Subject: [PATCH 01/15] bc: implement parsing --- Cargo.lock | 120 +++++ calc/Cargo.toml | 7 + calc/src/bc.rs | 39 ++ calc/src/bc_util/grammar.pest | 116 ++++ calc/src/bc_util/instructions.rs | 103 ++++ calc/src/bc_util/mod.rs | 11 + calc/src/bc_util/parser.rs | 900 +++++++++++++++++++++++++++++++ 7 files changed, 1296 insertions(+) create mode 100644 calc/src/bc.rs create mode 100644 calc/src/bc_util/grammar.pest create mode 100644 calc/src/bc_util/instructions.rs create mode 100644 calc/src/bc_util/mod.rs create mode 100644 calc/src/bc_util/parser.rs diff --git a/Cargo.lock b/Cargo.lock index 8bf591f93..ad3c21a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -224,6 +233,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -233,6 +251,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "deranged" version = "0.3.11" @@ -259,6 +287,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "4.0.0" @@ -341,6 +379,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -627,6 +675,51 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.2" @@ -676,7 +769,11 @@ dependencies = [ name = "posixutils-calc" version = "0.1.7" dependencies = [ + "clap", "gettext-rs", + "lazy_static", + "pest", + "pest_derive", "plib", "regex", ] @@ -946,6 +1043,17 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -1096,6 +1204,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uname" version = "0.1.1" diff --git a/calc/Cargo.toml b/calc/Cargo.toml index 36bb93131..a6c77ed38 100644 --- a/calc/Cargo.toml +++ b/calc/Cargo.toml @@ -7,8 +7,15 @@ edition = "2021" plib = { path = "../plib" } gettext-rs.workspace = true regex.workspace = true +clap.workspace = true +pest = "2.7.10" +pest_derive = "2.7.10" +lazy_static = "1.4.0" [[bin]] name = "expr" path = "src/expr.rs" +[[bin]] +name = "bc" +path = "src/bc.rs" diff --git a/calc/src/bc.rs b/calc/src/bc.rs new file mode 100644 index 000000000..1da27b26b --- /dev/null +++ b/calc/src/bc.rs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::ffi::OsString; + +use bc_util::parser::parse_program; + +mod bc_util; + +/// bc - arbitrary-precision arithmetic language +#[derive(Debug, clap::Parser)] +#[command(author, version, about, long_about)] +struct Args { + #[arg(short = 'l')] + define_math_functions: bool, + + files: Vec, +} + +fn main() { + loop { + let mut buf = String::new(); + std::io::stdin().read_line(&mut buf).unwrap(); + match parse_program(&buf) { + Ok(program) => { + println!("{:?}", program); + } + Err(e) => { + println!("{}", e); + } + } + } +} diff --git a/calc/src/bc_util/grammar.pest b/calc/src/bc_util/grammar.pest new file mode 100644 index 000000000..32f9e97ca --- /dev/null +++ b/calc/src/bc_util/grammar.pest @@ -0,0 +1,116 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +WHITESPACE = _{ " " | "\t" | "\\\n" } +COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" } + +letter = { 'a'..'z' } +digit = _{ '0'..'9' | 'A' .. 'F' } +string = { "\"" ~ (!("\"") ~ ANY)* ~ "\"" } +integer = { (digit)+ } +number = { + | (integer ~ "." ~ integer) + | ("." ~ integer) + | (integer ~ ".") + | integer +} +rel_op = { "==" | "<=" | ">=" | "!=" | "<" | ">" } +assign_op = { "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "^=" } + +neg = { "-" } + +binary_op = _{ + | add + | sub + | mul + | div + | modulus + | pow +} +add = { "+" } +sub = { "-" } +mul = { "*" } +div = { "/" } +modulus = { "%" } +pow = { "^" } + +builtin_fn = { "length" | "sqrt" | "scale" } + + +program = { SOI ~ input_item* ~ EOI } +input_item = { (semicolon_list ~ "\n") | function } +semicolon_list = { (statement ~ (";" ~ statement)*)? ~ ";"? } +statement_list = { (statement ~ ((";" | "\n") ~ statement)*)? ~ (";" | "\n")? } + +statement = { + | break_stmt + | quit + | return_stmt + | if_stmt + | while_stmt + | for_stmt + | scoped_statement_list + | string + | expression +} +break_stmt = { "break" } +quit = { "quit" } +return_stmt = { "return" ~ ( "(" ~ expression? ~ ")" )? } +for_stmt = { "for" ~ "(" ~ expression ~ ";" ~ condition ~ ";" ~ expression ~ ")" ~ statement } +if_stmt = { "if" ~ "(" ~ condition ~ ")" ~ statement } +while_stmt = { "while" ~ "(" ~ condition ~ ")" ~ statement } +scoped_statement_list = { "{" ~ statement_list ~ "}" } +condition = { relational_expression | expression } +relational_expression = { expression ~ rel_op ~ expression } + +function = { "define" ~ letter ~ "(" ~ parameter_list? ~ ")" ~ "{" ~ "\n" ~ auto_define_list? ~ statement_list ~ "}" } +parameter_list = { variable ~ ("," ~ variable)* } +variable = _{ array | variable_number } +variable_number = { letter } +array = { letter ~ "[" ~ "]" } + +auto_define_list = { "auto" ~ define_list ~ ("\n" | ";") } +define_list = { variable ~ ("," ~ variable)* } +argument_list = { expression | (letter ~ "[" ~ "]" ~ "," ~ argument_list) } + + +expression = { primary ~ (binary_op ~ primary)* } +primary = { + number + | paren + | builtin_call + | fn_call + | prefix_increment + | prefix_decrement + | postfix_increment + | postfix_decrement + | negation + | assignment + | named_expression +} +paren = { "(" ~ expression ~ ")" } +builtin_call = { builtin_fn ~ "(" ~ expression ~ ")" } +fn_call = { letter ~ "(" ~ argument_list? ~ ")" } +prefix_increment = { "++" ~ named_expression } +prefix_decrement = { "--" ~ named_expression } +postfix_increment = { named_expression ~ "++" } +postfix_decrement = { named_expression ~ "--" } +negation = { "-" ~ expression } +assignment = { named_expression ~ assign_op ~ expression } +named_expression = { + | scale + | ibase + | obase + | array_item + | variable_number +} +array_item = { letter ~ "[" ~ expression ~ "]" } +scale = { "scale" } +ibase = { "ibase" } +obase = { "obase" } diff --git a/calc/src/bc_util/instructions.rs b/calc/src/bc_util/instructions.rs new file mode 100644 index 000000000..22ab5ca0e --- /dev/null +++ b/calc/src/bc_util/instructions.rs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +#[derive(Debug, PartialEq, Eq)] +pub enum BuiltinFunction { + Length, + Sqrt, + Scale, +} + +#[derive(Debug, PartialEq)] +pub enum StmtInstruction { + Break, + Quit, + Return, + ReturnExpr(ExprInstruction), + If { + condition: ConditionInstruction, + body: Vec, + }, + While { + condition: ConditionInstruction, + body: Vec, + }, + String(String), + Expr(ExprInstruction), + DefineFunction { + name: char, + function: Function, + }, +} + +#[derive(Debug, PartialEq)] +pub enum NamedExpr { + Scale, + IBase, + OBase, + VariableNumber(char), + ArrayItem { + name: char, + index: Box, + }, +} + +#[derive(Debug, PartialEq)] +pub enum ExprInstruction { + Number(f64), + Named(NamedExpr), + Builtin { + function: BuiltinFunction, + arg: Box, + }, + PreIncrement(NamedExpr), + PreDecrement(NamedExpr), + PostIncrement(NamedExpr), + PostDecrement(NamedExpr), + Call { + name: char, + args: Vec, + }, + Assignment { + name: char, + value: Box, + }, + UnaryMinus(Box), + Add(Box, Box), + Sub(Box, Box), + Mul(Box, Box), + Div(Box, Box), + Mod(Box, Box), + Pow(Box, Box), +} + +#[derive(Debug, PartialEq)] +pub enum ConditionInstruction { + Expr(ExprInstruction), + Eq(ExprInstruction, ExprInstruction), + Ne(ExprInstruction, ExprInstruction), + Lt(ExprInstruction, ExprInstruction), + Leq(ExprInstruction, ExprInstruction), + Gt(ExprInstruction, ExprInstruction), + Geq(ExprInstruction, ExprInstruction), +} + +#[derive(Debug, PartialEq)] +pub enum Variable { + Number(char), + Array(char), +} + +#[derive(Default, Debug, PartialEq)] +pub struct Function { + pub name: char, + pub parameters: Vec, + pub locals: Vec, + pub body: Vec, +} diff --git a/calc/src/bc_util/mod.rs b/calc/src/bc_util/mod.rs new file mode 100644 index 000000000..8ed53461c --- /dev/null +++ b/calc/src/bc_util/mod.rs @@ -0,0 +1,11 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +pub mod instructions; +pub mod parser; diff --git a/calc/src/bc_util/parser.rs b/calc/src/bc_util/parser.rs new file mode 100644 index 000000000..c9dc8ed34 --- /dev/null +++ b/calc/src/bc_util/parser.rs @@ -0,0 +1,900 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use pest::{iterators::Pair, pratt_parser::PrattParser, Parser}; + +use super::instructions::*; + +lazy_static::lazy_static! { + static ref PRATT_PARSER: PrattParser = { + use pest::pratt_parser::{Assoc, Op}; + + // Precedence is defined lowest to highest + PrattParser::new() + .op(Op::infix(Rule::add, Assoc::Left) + | Op::infix(Rule::sub, Assoc::Left)) + .op(Op::infix(Rule::mul, Assoc::Left) + | Op::infix(Rule::div, Assoc::Left) + | Op::infix(Rule::modulus, Assoc::Left)) + .op(Op::infix(Rule::pow, Assoc::Right)) + .op(Op::prefix(Rule::neg)) + }; +} + +fn first_char(s: &str) -> char { + s.chars().next().unwrap() +} + +fn as_letter(r: Pair) -> char { + first_char(r.as_str()) +} + +fn first_child(r: Pair) -> Pair { + r.into_inner().next().unwrap() +} + +fn as_variable(r: Pair) -> Variable { + match r.as_rule() { + Rule::variable_number => Variable::Number(as_letter(r)), + Rule::array => Variable::Array(as_letter(r)), + _ => unreachable!(), + } +} + +fn parse_named_expr(expr: Pair) -> NamedExpr { + let expr = first_child(expr); + match expr.as_rule() { + Rule::scale => NamedExpr::Scale, + Rule::ibase => NamedExpr::IBase, + Rule::obase => NamedExpr::OBase, + Rule::variable_number => NamedExpr::VariableNumber(first_char(expr.as_str())), + Rule::array_item => { + // name [ index ] + let mut inner = expr.into_inner(); + let name = as_letter(inner.next().unwrap()); + let index = Box::new(parse_expr(inner.next().unwrap())); + NamedExpr::ArrayItem { name, index } + } + _ => unreachable!(), + } +} + +fn parse_primary(expr: Pair) -> ExprInstruction { + let expr = expr.into_inner().next().unwrap(); + match expr.as_rule() { + Rule::number => ExprInstruction::Number(expr.as_str().trim().parse().unwrap()), + Rule::paren => parse_expr(expr), + Rule::builtin_call => { + let mut inner = expr.into_inner(); + let func = match inner.next().unwrap().as_str() { + "length" => BuiltinFunction::Length, + "sqrt" => BuiltinFunction::Sqrt, + "scale" => BuiltinFunction::Scale, + _ => unreachable!(), + }; + let arg = parse_expr(inner.next().unwrap()); + ExprInstruction::Builtin { + function: func, + arg: Box::new(arg), + } + } + Rule::fn_call => { + // name ( expr* ) + let mut inner = expr.into_inner(); + let name = as_letter(inner.next().unwrap()); + let args = inner.next().unwrap().into_inner().map(parse_expr).collect(); + ExprInstruction::Call { name, args } + } + Rule::prefix_increment => { + ExprInstruction::PreIncrement(parse_named_expr(first_child(expr))) + } + Rule::prefix_decrement => { + ExprInstruction::PreDecrement(parse_named_expr(first_child(expr))) + } + Rule::postfix_increment => { + ExprInstruction::PostIncrement(parse_named_expr(first_child(expr))) + } + Rule::postfix_decrement => { + ExprInstruction::PostDecrement(parse_named_expr(first_child(expr))) + } + Rule::negation => ExprInstruction::UnaryMinus(Box::new(parse_expr(first_child(expr)))), + Rule::assignment => { + // name assignment_operator expr + let mut inner = expr.into_inner(); + let name = as_letter(inner.next().unwrap()); + let op = inner.next().unwrap(); + let value = parse_expr(inner.next().unwrap()); + match op.as_str() { + "=" => ExprInstruction::Assignment { + name, + value: Box::new(value), + }, + "+=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Add( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + "-=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Sub( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + "*=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Mul( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + "/=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Div( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + "%=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Mod( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + "^=" => ExprInstruction::Assignment { + name, + value: Box::new(ExprInstruction::Pow( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), + Box::new(value), + )), + }, + _ => unreachable!(), + } + } + Rule::named_expression => ExprInstruction::Named(parse_named_expr(expr)), + _ => unreachable!(), + } +} + +fn parse_expr(expr: Pair) -> ExprInstruction { + PRATT_PARSER + .map_primary(parse_primary) + .map_prefix(|op, rhs| match op.as_rule() { + Rule::neg => ExprInstruction::UnaryMinus(Box::new(rhs)), + _ => unreachable!(), + }) + .map_infix(|lhs, op, rhs| match op.as_rule() { + Rule::add => ExprInstruction::Add(Box::new(lhs), Box::new(rhs)), + Rule::sub => ExprInstruction::Sub(Box::new(lhs), Box::new(rhs)), + Rule::mul => ExprInstruction::Mul(Box::new(lhs), Box::new(rhs)), + Rule::div => ExprInstruction::Div(Box::new(lhs), Box::new(rhs)), + Rule::modulus => ExprInstruction::Mod(Box::new(lhs), Box::new(rhs)), + Rule::pow => ExprInstruction::Pow(Box::new(lhs), Box::new(rhs)), + _ => unreachable!(), + }) + .parse(expr.into_inner()) +} + +fn parse_condition(expr: Pair) -> ConditionInstruction { + let expr = first_child(expr); + match expr.as_rule() { + Rule::expression => ConditionInstruction::Expr(parse_expr(expr)), + Rule::relational_expression => { + let mut inner = expr.into_inner(); + let left = parse_expr(inner.next().unwrap()); + let op = inner.next().unwrap(); + let right = parse_expr(inner.next().unwrap()); + match op.as_str() { + "==" => ConditionInstruction::Eq(left, right), + "!=" => ConditionInstruction::Ne(left, right), + "<" => ConditionInstruction::Lt(left, right), + "<=" => ConditionInstruction::Leq(left, right), + ">" => ConditionInstruction::Gt(left, right), + ">=" => ConditionInstruction::Geq(left, right), + _ => unreachable!(), + } + } + _ => unreachable!(), + } +} + +fn parse_stmt(stmt: Pair, statements: &mut Vec) { + let stmt = first_child(stmt); + match stmt.as_rule() { + Rule::break_stmt => { + statements.push(StmtInstruction::Break); + } + Rule::quit => { + statements.push(StmtInstruction::Quit); + } + Rule::return_stmt => { + let mut inner = stmt.into_inner(); + if let Some(expr) = inner.next() { + statements.push(StmtInstruction::ReturnExpr(parse_expr(expr))); + } else { + statements.push(StmtInstruction::Return); + } + } + Rule::if_stmt => { + let mut inner = stmt.into_inner(); + let condition = parse_condition(inner.next().unwrap()); + let mut body = Vec::new(); + parse_stmt(inner.next().unwrap(), &mut body); + statements.push(StmtInstruction::If { condition, body }); + } + Rule::while_stmt => { + let mut inner = stmt.into_inner(); + let condition = parse_condition(inner.next().unwrap()); + let mut body = Vec::new(); + parse_stmt(inner.next().unwrap(), &mut body); + statements.push(StmtInstruction::While { condition, body }); + } + Rule::for_stmt => { + let mut inner = stmt.into_inner(); + let init = parse_expr(inner.next().unwrap()); + statements.push(StmtInstruction::Expr(init)); + let condition = parse_condition(inner.next().unwrap()); + let after = parse_expr(inner.next().unwrap()); + let mut body = Vec::new(); + parse_stmt(inner.next().unwrap(), &mut body); + body.push(StmtInstruction::Expr(after)); + statements.push(StmtInstruction::While { condition, body }); + } + + Rule::scoped_statement_list => { + for stmt in first_child(stmt).into_inner() { + parse_stmt(stmt, statements); + } + } + Rule::string => { + statements.push(StmtInstruction::String( + stmt.as_str().trim_matches('\"').to_string(), + )); + } + Rule::expression => { + statements.push(StmtInstruction::Expr(parse_expr(stmt))); + } + _ => unreachable!(), + } +} + +fn parse_function(func: Pair) -> Function { + let mut function = func.into_inner(); + let name = as_letter(function.next().unwrap()); + + let mut parameters = Vec::new(); + let parameter_list = function.next().unwrap(); + let auto_define_list = if parameter_list.as_rule() == Rule::parameter_list { + parameters = parameter_list.into_inner().map(as_variable).collect(); + function.next().unwrap() + } else { + parameter_list + }; + + let mut locals = Vec::new(); + let statement_list = if auto_define_list.as_rule() == Rule::auto_define_list { + locals = first_child(auto_define_list) + .into_inner() + .map(as_variable) + .collect(); + function.next().unwrap() + } else { + auto_define_list + }; + + let mut body = Vec::new(); + for stmt in statement_list.into_inner() { + parse_stmt(stmt, &mut body); + } + Function { + name, + parameters, + locals, + body, + } +} + +#[derive(pest_derive::Parser)] +#[grammar = "bc_util/grammar.pest"] +pub struct BcParser; + +pub type Program = Vec; + +pub fn parse_program(text: &str) -> Result> { + let program = BcParser::parse(Rule::program, text)?.next().unwrap(); + let mut result = Vec::new(); + let input_items = first_child(program); + for item in input_items.into_inner() { + match item.as_rule() { + Rule::semicolon_list => { + for stmt in item.into_inner() { + parse_stmt(stmt, &mut result); + } + } + Rule::function => { + let f = parse_function(item); + result.push(StmtInstruction::DefineFunction { + name: f.name, + function: f, + }); + } + _ => unreachable!(), + } + } + Ok(result) +} + +#[cfg(test)] +mod test { + use super::*; + + fn parse_expr(input: &str) -> ExprInstruction { + let program = parse_program(input).expect("error parsing expression"); + assert_eq!(program.len(), 1); + if let StmtInstruction::Expr(expr) = program.into_iter().next().unwrap() { + expr + } else { + panic!("expected expression") + } + } + + fn parse_stmt(input: &str) -> StmtInstruction { + let program = parse_program(input).expect("error parsing statement"); + assert_eq!(program.len(), 1); + program.into_iter().next().unwrap() + } + + fn parse_function(input: &str) -> Function { + let program = parse_program(input).expect("error parsing function"); + assert_eq!(program.len(), 1); + if let StmtInstruction::DefineFunction { function, .. } = + program.into_iter().next().unwrap() + { + function + } else { + panic!("expected function") + } + } + + #[test] + fn test_parse_number() { + let expr = parse_expr("123\n"); + assert_eq!(expr, ExprInstruction::Number(123.0)); + let expr = parse_expr("123.456\n"); + assert_eq!(expr, ExprInstruction::Number(123.456)); + let expr = parse_expr(".456\n"); + assert_eq!(expr, ExprInstruction::Number(0.456)); + let expr = parse_expr("123.\n"); + assert_eq!(expr, ExprInstruction::Number(123.0)); + } + + #[test] + fn test_parse_named() { + let expr = parse_expr("scale\n"); + assert_eq!(expr, ExprInstruction::Named(NamedExpr::Scale)); + let expr = parse_expr("ibase\n"); + assert_eq!(expr, ExprInstruction::Named(NamedExpr::IBase)); + let expr = parse_expr("obase\n"); + assert_eq!(expr, ExprInstruction::Named(NamedExpr::OBase)); + let expr = parse_expr("a\n"); + assert_eq!(expr, ExprInstruction::Named(NamedExpr::VariableNumber('a'))); + let expr = parse_expr("a[1]\n"); + assert_eq!( + expr, + (ExprInstruction::Named(NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number(1.0)) + })) + ); + } + + #[test] + fn test_parse_builtin_call() { + let expr = parse_expr("length(123)\n"); + assert_eq!( + expr, + (ExprInstruction::Builtin { + function: BuiltinFunction::Length, + arg: Box::new(ExprInstruction::Number(123.0)) + }) + ); + let expr = parse_expr("sqrt(123)\n"); + assert_eq!( + expr, + (ExprInstruction::Builtin { + function: BuiltinFunction::Sqrt, + arg: Box::new(ExprInstruction::Number(123.0)) + }) + ); + let expr = parse_expr("scale(123)\n"); + assert_eq!( + expr, + (ExprInstruction::Builtin { + function: BuiltinFunction::Scale, + arg: Box::new(ExprInstruction::Number(123.0)) + }) + ); + } + + #[test] + fn test_parse_pre_increment() { + let expr = parse_expr("++d\n"); + assert_eq!( + expr, + ExprInstruction::PreIncrement(NamedExpr::VariableNumber('d')) + ); + } + + #[test] + fn test_parse_pre_decrement() { + let expr = parse_expr("--g\n"); + assert_eq!( + expr, + ExprInstruction::PreDecrement(NamedExpr::VariableNumber('g')) + ); + } + + #[test] + fn test_parse_post_increment() { + let expr = parse_expr("e++\n"); + assert_eq!( + expr, + ExprInstruction::PostIncrement(NamedExpr::VariableNumber('e')) + ); + } + + #[test] + fn test_parse_post_decrement() { + let expr = parse_expr("f--\n"); + assert_eq!( + expr, + ExprInstruction::PostDecrement(NamedExpr::VariableNumber('f')) + ); + } + + #[test] + fn test_parse_fn_call() { + let expr = parse_expr("a(1)\n"); + assert_eq!( + expr, + ExprInstruction::Call { + name: 'a', + args: vec![ExprInstruction::Number(1.0),] + } + ); + } + + #[test] + fn test_parse_simple_assignment() { + let expr = parse_expr("a = 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Number(1.0)) + }) + ); + } + + #[test] + fn test_parse_compound_assignment() { + let expr = parse_expr("a += 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Add( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number(1.0)) + )) + }) + ); + let expr = parse_expr("a -= 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Sub( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number(1.0)) + )) + }) + ); + let expr = parse_expr("a *= 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Mul( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number(1.0)) + )) + }) + ); + let expr = parse_expr("a /= 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Div( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number(1.0)) + )) + }) + ); + let expr = parse_expr("a ^= 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Pow( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number(1.0)) + )) + }) + ); + } + + #[test] + fn test_parse_unary_minus() { + let expr = parse_expr("-1\n"); + assert_eq!( + expr, + ExprInstruction::UnaryMinus(Box::new(ExprInstruction::Number(1.0))) + ); + } + + #[test] + fn test_parse_binary_op() { + let expr = parse_expr("1 + 2\n"); + assert_eq!( + expr, + (ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + let expr = parse_expr("1 ^ 2\n"); + assert_eq!( + expr, + (ExprInstruction::Pow( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + } + + #[test] + fn test_parse_correct_precedence() { + let expr = parse_expr("1 + 2 * 3\n"); + assert_eq!( + expr, + ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Mul( + Box::new(ExprInstruction::Number(2.0)), + Box::new(ExprInstruction::Number(3.0)) + )) + ) + ); + let expr = parse_expr("1 * 2 + 3\n"); + assert_eq!( + expr, + ExprInstruction::Add( + Box::new(ExprInstruction::Mul( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )), + Box::new(ExprInstruction::Number(3.0)) + ) + ); + let expr = parse_expr("1 * 2 ^ 3\n"); + assert_eq!( + expr, + ExprInstruction::Mul( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Pow( + Box::new(ExprInstruction::Number(2.0)), + Box::new(ExprInstruction::Number(3.0)) + )) + ) + ); + } + + #[test] + fn test_pow_is_right_associative() { + let expr = parse_expr("1 ^ 2 ^ 3\n"); + assert_eq!( + expr, + ExprInstruction::Pow( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Pow( + Box::new(ExprInstruction::Number(2.0)), + Box::new(ExprInstruction::Number(3.0)) + )) + ) + ); + } + + #[test] + fn test_parse_break() { + let stmt = parse_stmt("break\n"); + assert_eq!(stmt, StmtInstruction::Break); + } + + #[test] + fn test_parse_quit() { + let stmt = parse_stmt("quit\n"); + assert_eq!(stmt, StmtInstruction::Quit); + } + + #[test] + fn test_parse_empty_return() { + let stmt = parse_stmt("return\n"); + assert_eq!(stmt, StmtInstruction::Return); + } + + #[test] + fn test_parse_return_expr() { + let stmt = parse_stmt("return(1)\n"); + assert_eq!( + stmt, + StmtInstruction::ReturnExpr(ExprInstruction::Number(1.0)) + ); + } + + #[test] + fn test_parse_if() { + let stmt = parse_stmt("if (x <= z) a = 2\n"); + assert_eq!( + stmt, + StmtInstruction::If { + condition: ConditionInstruction::Leq( + ExprInstruction::Named(NamedExpr::VariableNumber('x')), + ExprInstruction::Named(NamedExpr::VariableNumber('z')) + ), + body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Number(2.0)) + })] + } + ); + } + + #[test] + fn test_parse_while() { + let stmt = parse_stmt("while (x <= z) a = 2\n"); + assert_eq!( + stmt, + StmtInstruction::While { + condition: ConditionInstruction::Leq( + ExprInstruction::Named(NamedExpr::VariableNumber('x')), + ExprInstruction::Named(NamedExpr::VariableNumber('z')) + ), + body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Number(2.0)) + })] + } + ); + } + + #[test] + fn test_parse_for() { + let instructions = + parse_program("for (i = 0; i < 10; i++) a = 2\n").expect("error parsing for loop"); + assert_eq!(instructions.len(), 2); + assert_eq!( + instructions[0], + StmtInstruction::Expr(ExprInstruction::Assignment { + name: 'i', + value: Box::new(ExprInstruction::Number(0.0)) + }) + ); + assert_eq!( + instructions[1], + StmtInstruction::While { + condition: ConditionInstruction::Lt( + ExprInstruction::Named(NamedExpr::VariableNumber('i')), + ExprInstruction::Number(10.0) + ), + body: vec![ + StmtInstruction::Expr(ExprInstruction::Assignment { + name: 'a', + value: Box::new(ExprInstruction::Number(2.0)) + }), + StmtInstruction::Expr(ExprInstruction::PostIncrement( + NamedExpr::VariableNumber('i') + )) + ] + } + ); + } + + #[test] + fn test_parse_empty_scoped_statement_list() { + let instructions = + parse_program("{ }\n").expect("error parsing empty scoped statement list"); + assert_eq!(instructions.len(), 0); + } + + #[test] + fn test_parse_scoped_statement_list() { + let instructions = parse_program("{ 1 + 2; 3 + 4; \"string\" }\n") + .expect("error parsing scoped statement list"); + assert_eq!(instructions.len(), 3); + assert_eq!( + instructions[0], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + assert_eq!( + instructions[1], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(3.0)), + Box::new(ExprInstruction::Number(4.0)) + )) + ); + assert_eq!( + instructions[2], + StmtInstruction::String("string".to_string()) + ); + } + + #[test] + fn test_parse_string() { + let stmt = parse_stmt("\"hello\"\n"); + assert_eq!(stmt, StmtInstruction::String("hello".to_string())); + } + + #[test] + fn test_parse_multiline_string() { + let stmt = parse_stmt("\"hello\nworld\"\n"); + assert_eq!(stmt, StmtInstruction::String("hello\nworld".to_string())); + } + + #[test] + fn test_parse_expression() { + let stmt = parse_stmt("1 + 2\n"); + assert_eq!( + stmt, + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + } + + #[test] + fn test_parse_multiple_statements_on_single_line() { + let instructions = + parse_program("1 + 2; 3 + 4; \"string\"\n").expect("error parsing multiple statements"); + assert_eq!(instructions.len(), 3); + assert_eq!( + instructions[0], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + assert_eq!( + instructions[1], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(3.0)), + Box::new(ExprInstruction::Number(4.0)) + )) + ); + assert_eq!( + instructions[2], + StmtInstruction::String("string".to_string()) + ); + } + + #[test] + fn test_parse_function_empty_no_parameters() { + let func = parse_function("define f() {\n }\n"); + assert_eq!( + func, + Function { + name: 'f', + ..Default::default() + } + ); + } + + #[test] + fn test_parse_function_empty_with_parameters() { + let func = parse_function("define f(a[], b, c, d[]) {\n }\n"); + assert_eq!( + func, + Function { + name: 'f', + parameters: vec![ + Variable::Array('a'), + Variable::Number('b'), + Variable::Number('c'), + Variable::Array('d') + ], + ..Default::default() + } + ); + } + + #[test] + fn test_parse_function_with_locals() { + let func = parse_function("define f() {\n auto a[], b, c, d[]\n}\n"); + assert_eq!( + func, + Function { + name: 'f', + locals: vec![ + Variable::Array('a'), + Variable::Number('b'), + Variable::Number('c'), + Variable::Array('d') + ], + ..Default::default() + } + ); + } + + #[test] + fn test_parse_function_with_statements() { + let func = parse_function("define f() {\n 1 + 2\n}\n"); + assert_eq!( + func, + Function { + name: 'f', + body: vec![StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + ))], + ..Default::default() + } + ); + } + + #[test] + fn test_ignore_comments() { + let instructions = parse_program("1 + 2; /*multiline\ncomment*/3 + 4\n") + .expect("error parsing multiple statements with comments"); + assert_eq!(instructions.len(), 2); + assert_eq!( + instructions[0], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + assert_eq!( + instructions[1], + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(3.0)), + Box::new(ExprInstruction::Number(4.0)) + )) + ); + } + + #[test] + fn test_ignore_backslash_newline() { + let stmt = parse_stmt("1 + \\\n2\n"); + assert_eq!( + stmt, + StmtInstruction::Expr(ExprInstruction::Add( + Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number(2.0)) + )) + ); + } +} From b79a703685a2e242df6e6f4e3ec56774899e491e Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 16:00:46 +0200 Subject: [PATCH 02/15] bc: finish basic implementation --- Cargo.lock | 39 + calc/Cargo.toml | 1 + calc/src/bc.rs | 58 +- calc/src/bc_util/grammar.pest | 42 +- calc/src/bc_util/instructions.rs | 47 +- calc/src/bc_util/interpreter.rs | 1054 +++++++++++++++++ calc/src/bc_util/mod.rs | 2 + calc/src/bc_util/number.rs | 524 ++++++++ calc/src/bc_util/parser.rs | 695 ++++++++--- calc/tests/bc/add.bc | 5 + calc/tests/bc/add.out | 4 + .../arrays_are_passed_to_function_by_value.bc | 13 + ...arrays_are_passed_to_function_by_value.out | 5 + calc/tests/bc/assign_to_array_item.bc | 5 + calc/tests/bc/assign_to_array_item.out | 2 + ...o_function_local_does_not_change_global.bc | 21 + ..._function_local_does_not_change_global.out | 6 + calc/tests/bc/assign_to_variable.bc | 5 + calc/tests/bc/assign_to_variable.out | 2 + ...e_value_to_base_register_is_hexadecimal.bc | 13 + ..._value_to_base_register_is_hexadecimal.out | 3 + calc/tests/bc/break.out | 0 calc/tests/bc/break_out_of_loop.bc | 3 + calc/tests/bc/break_out_of_loop.out | 0 calc/tests/bc/comments.bc | 6 + calc/tests/bc/comments.out | 0 calc/tests/bc/compound_assignment.bc | 14 + calc/tests/bc/compound_assignment.out | 6 + calc/tests/bc/define_empty_function.bc | 3 + calc/tests/bc/define_empty_function.out | 0 calc/tests/bc/define_function_with_locals.bc | 4 + calc/tests/bc/define_function_with_locals.out | 0 .../bc/define_function_with_parameters.bc | 3 + .../bc/define_function_with_parameters.out | 0 calc/tests/bc/div.bc | 22 + calc/tests/bc/div.out | 10 + calc/tests/bc/empty_return_returns_zero.bc | 5 + calc/tests/bc/empty_return_returns_zero.out | 1 + calc/tests/bc/for.out | 10 + calc/tests/bc/for_loop.bc | 2 + calc/tests/bc/for_loop.out | 10 + .../bc/function_returns_correct_value.bc | 6 + .../bc/function_returns_correct_value.out | 1 + .../function_with_no_return_returns_zero.bc | 6 + .../function_with_no_return_returns_zero.out | 1 + calc/tests/bc/if.bc | 5 + calc/tests/bc/if.out | 3 + calc/tests/bc/length.bc | 6 + calc/tests/bc/length.out | 5 + calc/tests/bc/mod.bc | 24 + calc/tests/bc/mod.out | 9 + calc/tests/bc/mul.bc | 28 + calc/tests/bc/mul.out | 11 + calc/tests/bc/multiline_numbers.bc | 13 + calc/tests/bc/multiline_numbers.out | 4 + calc/tests/bc/operator_precedence.bc | 19 + calc/tests/bc/operator_precedence.out | 10 + calc/tests/bc/output_base_1097.bc | 6 + calc/tests/bc/output_base_1097.out | 4 + calc/tests/bc/output_base_14.bc | 6 + calc/tests/bc/output_base_14.out | 4 + calc/tests/bc/output_base_6.bc | 6 + calc/tests/bc/output_base_6.out | 4 + calc/tests/bc/output_base_67.bc | 6 + calc/tests/bc/output_base_67.out | 4 + calc/tests/bc/postfix_decrement.bc | 3 + calc/tests/bc/postfix_decrement.out | 1 + calc/tests/bc/postfix_increment.bc | 3 + calc/tests/bc/postfix_increment.out | 1 + calc/tests/bc/pow.bc | 44 + calc/tests/bc/pow.out | 23 + calc/tests/bc/prefix_decrement.bc | 3 + calc/tests/bc/prefix_decrement.out | 1 + calc/tests/bc/prefix_increment.bc | 3 + calc/tests/bc/prefix_increment.out | 1 + calc/tests/bc/quit.bc | 1 + calc/tests/bc/quit.out | 0 calc/tests/bc/quit_in_unexecuted_code.bc | 9 + calc/tests/bc/quit_in_unexecuted_code.out | 0 calc/tests/bc/read_base_10.bc | 8 + calc/tests/bc/read_base_10.out | 7 + calc/tests/bc/read_base_15.bc | 6 + calc/tests/bc/read_base_15.out | 4 + calc/tests/bc/read_base_2.bc | 6 + calc/tests/bc/read_base_2.out | 4 + calc/tests/bc/scale.bc | 6 + calc/tests/bc/scale.out | 5 + calc/tests/bc/sqrt.bc | 30 + calc/tests/bc/sqrt.out | 14 + calc/tests/bc/strings.bc | 4 + calc/tests/bc/strings.out | 2 + calc/tests/bc/sub.bc | 5 + calc/tests/bc/sub.out | 4 + calc/tests/bc/unary_minus.bc | 8 + calc/tests/bc/unary_minus.out | 5 + .../bc/uninitialized_variables_are_zero.bc | 2 + .../bc/uninitialized_variables_are_zero.out | 1 + calc/tests/bc/while.out | 10 + calc/tests/bc/while_loop.bc | 3 + calc/tests/bc/while_loop.out | 10 + calc/tests/integration.rs | 240 ++++ 101 files changed, 3093 insertions(+), 220 deletions(-) create mode 100644 calc/src/bc_util/interpreter.rs create mode 100644 calc/src/bc_util/number.rs create mode 100644 calc/tests/bc/add.bc create mode 100644 calc/tests/bc/add.out create mode 100644 calc/tests/bc/arrays_are_passed_to_function_by_value.bc create mode 100644 calc/tests/bc/arrays_are_passed_to_function_by_value.out create mode 100644 calc/tests/bc/assign_to_array_item.bc create mode 100644 calc/tests/bc/assign_to_array_item.out create mode 100644 calc/tests/bc/assign_to_function_local_does_not_change_global.bc create mode 100644 calc/tests/bc/assign_to_function_local_does_not_change_global.out create mode 100644 calc/tests/bc/assign_to_variable.bc create mode 100644 calc/tests/bc/assign_to_variable.out create mode 100644 calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.bc create mode 100644 calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.out create mode 100644 calc/tests/bc/break.out create mode 100644 calc/tests/bc/break_out_of_loop.bc create mode 100644 calc/tests/bc/break_out_of_loop.out create mode 100644 calc/tests/bc/comments.bc create mode 100644 calc/tests/bc/comments.out create mode 100644 calc/tests/bc/compound_assignment.bc create mode 100644 calc/tests/bc/compound_assignment.out create mode 100644 calc/tests/bc/define_empty_function.bc create mode 100644 calc/tests/bc/define_empty_function.out create mode 100644 calc/tests/bc/define_function_with_locals.bc create mode 100644 calc/tests/bc/define_function_with_locals.out create mode 100644 calc/tests/bc/define_function_with_parameters.bc create mode 100644 calc/tests/bc/define_function_with_parameters.out create mode 100644 calc/tests/bc/div.bc create mode 100644 calc/tests/bc/div.out create mode 100644 calc/tests/bc/empty_return_returns_zero.bc create mode 100644 calc/tests/bc/empty_return_returns_zero.out create mode 100644 calc/tests/bc/for.out create mode 100644 calc/tests/bc/for_loop.bc create mode 100644 calc/tests/bc/for_loop.out create mode 100644 calc/tests/bc/function_returns_correct_value.bc create mode 100644 calc/tests/bc/function_returns_correct_value.out create mode 100644 calc/tests/bc/function_with_no_return_returns_zero.bc create mode 100644 calc/tests/bc/function_with_no_return_returns_zero.out create mode 100644 calc/tests/bc/if.bc create mode 100644 calc/tests/bc/if.out create mode 100644 calc/tests/bc/length.bc create mode 100644 calc/tests/bc/length.out create mode 100644 calc/tests/bc/mod.bc create mode 100644 calc/tests/bc/mod.out create mode 100644 calc/tests/bc/mul.bc create mode 100644 calc/tests/bc/mul.out create mode 100644 calc/tests/bc/multiline_numbers.bc create mode 100644 calc/tests/bc/multiline_numbers.out create mode 100644 calc/tests/bc/operator_precedence.bc create mode 100644 calc/tests/bc/operator_precedence.out create mode 100644 calc/tests/bc/output_base_1097.bc create mode 100644 calc/tests/bc/output_base_1097.out create mode 100644 calc/tests/bc/output_base_14.bc create mode 100644 calc/tests/bc/output_base_14.out create mode 100644 calc/tests/bc/output_base_6.bc create mode 100644 calc/tests/bc/output_base_6.out create mode 100644 calc/tests/bc/output_base_67.bc create mode 100644 calc/tests/bc/output_base_67.out create mode 100644 calc/tests/bc/postfix_decrement.bc create mode 100644 calc/tests/bc/postfix_decrement.out create mode 100644 calc/tests/bc/postfix_increment.bc create mode 100644 calc/tests/bc/postfix_increment.out create mode 100644 calc/tests/bc/pow.bc create mode 100644 calc/tests/bc/pow.out create mode 100644 calc/tests/bc/prefix_decrement.bc create mode 100644 calc/tests/bc/prefix_decrement.out create mode 100644 calc/tests/bc/prefix_increment.bc create mode 100644 calc/tests/bc/prefix_increment.out create mode 100644 calc/tests/bc/quit.bc create mode 100644 calc/tests/bc/quit.out create mode 100644 calc/tests/bc/quit_in_unexecuted_code.bc create mode 100644 calc/tests/bc/quit_in_unexecuted_code.out create mode 100644 calc/tests/bc/read_base_10.bc create mode 100644 calc/tests/bc/read_base_10.out create mode 100644 calc/tests/bc/read_base_15.bc create mode 100644 calc/tests/bc/read_base_15.out create mode 100644 calc/tests/bc/read_base_2.bc create mode 100644 calc/tests/bc/read_base_2.out create mode 100644 calc/tests/bc/scale.bc create mode 100644 calc/tests/bc/scale.out create mode 100644 calc/tests/bc/sqrt.bc create mode 100644 calc/tests/bc/sqrt.out create mode 100644 calc/tests/bc/strings.bc create mode 100644 calc/tests/bc/strings.out create mode 100644 calc/tests/bc/sub.bc create mode 100644 calc/tests/bc/sub.out create mode 100644 calc/tests/bc/unary_minus.bc create mode 100644 calc/tests/bc/unary_minus.out create mode 100644 calc/tests/bc/uninitialized_variables_are_zero.bc create mode 100644 calc/tests/bc/uninitialized_variables_are_zero.out create mode 100644 calc/tests/bc/while.out create mode 100644 calc/tests/bc/while_loop.bc create mode 100644 calc/tests/bc/while_loop.out diff --git a/Cargo.lock b/Cargo.lock index ad3c21a7e..d16a0398d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,19 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bigdecimal" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bitflags" version = "2.5.0" @@ -521,6 +534,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "libredox" version = "0.1.3" @@ -596,12 +615,31 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-bigint" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -769,6 +807,7 @@ dependencies = [ name = "posixutils-calc" version = "0.1.7" dependencies = [ + "bigdecimal", "clap", "gettext-rs", "lazy_static", diff --git a/calc/Cargo.toml b/calc/Cargo.toml index a6c77ed38..b49c51211 100644 --- a/calc/Cargo.toml +++ b/calc/Cargo.toml @@ -11,6 +11,7 @@ clap.workspace = true pest = "2.7.10" pest_derive = "2.7.10" lazy_static = "1.4.0" +bigdecimal = "0.4.3" [[bin]] name = "expr" diff --git a/calc/src/bc.rs b/calc/src/bc.rs index 1da27b26b..d054a1ab0 100644 --- a/calc/src/bc.rs +++ b/calc/src/bc.rs @@ -9,12 +9,16 @@ use std::ffi::OsString; -use bc_util::parser::parse_program; +use bc_util::{ + interpreter::Interpreter, + parser::{is_incomplete, parse_program}, +}; +use clap::Parser; mod bc_util; /// bc - arbitrary-precision arithmetic language -#[derive(Debug, clap::Parser)] +#[derive(Debug, Parser)] #[command(author, version, about, long_about)] struct Args { #[arg(short = 'l')] @@ -23,17 +27,53 @@ struct Args { files: Vec, } -fn main() { - loop { - let mut buf = String::new(); - std::io::stdin().read_line(&mut buf).unwrap(); - match parse_program(&buf) { - Ok(program) => { - println!("{:?}", program); +fn exec_str(s: &str, interpreter: &mut Interpreter) -> bool { + match parse_program(s) { + Ok(program) => match interpreter.exec(program) { + Ok(output) => { + print!("{}", output.string); + if output.has_quit { + return true; + } } Err(e) => { println!("{}", e); } + }, + Err(e) => { + println!("{}", e); + } + } + false +} + +fn main() { + let args = Args::parse(); + let mut interpreter = Interpreter::default(); + for file in args.files { + match std::fs::read_to_string(&file) { + Ok(s) => { + if exec_str(&s, &mut interpreter) { + return; + } + } + Err(_) => { + eprintln!("Could not read file: {}", file.to_string_lossy()); + return; + } + }; + } + + let mut buf = String::new(); + loop { + std::io::stdin().read_line(&mut buf).unwrap(); + if is_incomplete(&buf) { + continue; + } else { + if exec_str(&buf, &mut interpreter) { + return; + } + buf.clear() } } } diff --git a/calc/src/bc_util/grammar.pest b/calc/src/bc_util/grammar.pest index 32f9e97ca..fb7965fce 100644 --- a/calc/src/bc_util/grammar.pest +++ b/calc/src/bc_util/grammar.pest @@ -8,20 +8,28 @@ // WHITESPACE = _{ " " | "\t" | "\\\n" } -COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" } +COMMENT = _{ ("/*" ~ (!"*/" ~ ANY)* ~ "*/") } letter = { 'a'..'z' } digit = _{ '0'..'9' | 'A' .. 'F' } string = { "\"" ~ (!("\"") ~ ANY)* ~ "\"" } -integer = { (digit)+ } -number = { +integer = { ( digit | "\\\n" )+ } +number = @{ | (integer ~ "." ~ integer) | ("." ~ integer) | (integer ~ ".") | integer } rel_op = { "==" | "<=" | ">=" | "!=" | "<" | ">" } -assign_op = { "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "^=" } +assign_op = _{ assign | add_assign | sub_assign | mul_assign | div_assign | mod_assign | pow_assign } +assign = { "=" } +add_assign = { "+=" } +sub_assign = { "-=" } +mul_assign = { "*=" } +div_assign = { "/=" } +mod_assign = { "%=" } +pow_assign = { "^=" } + neg = { "-" } @@ -42,11 +50,10 @@ pow = { "^" } builtin_fn = { "length" | "sqrt" | "scale" } - program = { SOI ~ input_item* ~ EOI } -input_item = { (semicolon_list ~ "\n") | function } +input_item = _{ (semicolon_list ~ "\n") | function } semicolon_list = { (statement ~ (";" ~ statement)*)? ~ ";"? } -statement_list = { (statement ~ ((";" | "\n") ~ statement)*)? ~ (";" | "\n")? } +statement_list = { (";" | "\n")* ~ (statement ~ (";" | "\n")*)* } statement = { | break_stmt @@ -55,7 +62,7 @@ statement = { | if_stmt | while_stmt | for_stmt - | scoped_statement_list + | braced_statement_list | string | expression } @@ -65,7 +72,7 @@ return_stmt = { "return" ~ ( "(" ~ expression? ~ ")" )? } for_stmt = { "for" ~ "(" ~ expression ~ ";" ~ condition ~ ";" ~ expression ~ ")" ~ statement } if_stmt = { "if" ~ "(" ~ condition ~ ")" ~ statement } while_stmt = { "while" ~ "(" ~ condition ~ ")" ~ statement } -scoped_statement_list = { "{" ~ statement_list ~ "}" } +braced_statement_list = { "{" ~ statement_list ~ "}" } condition = { relational_expression | expression } relational_expression = { expression ~ rel_op ~ expression } @@ -77,8 +84,8 @@ array = { letter ~ "[" ~ "]" } auto_define_list = { "auto" ~ define_list ~ ("\n" | ";") } define_list = { variable ~ ("," ~ variable)* } -argument_list = { expression | (letter ~ "[" ~ "]" ~ "," ~ argument_list) } - +argument_list = { argument ~ ("," ~ argument)* } +argument = _{ array | expression } expression = { primary ~ (binary_op ~ primary)* } primary = { @@ -91,6 +98,8 @@ primary = { | postfix_increment | postfix_decrement | negation + | register_assignment + | register | assignment | named_expression } @@ -101,16 +110,19 @@ prefix_increment = { "++" ~ named_expression } prefix_decrement = { "--" ~ named_expression } postfix_increment = { named_expression ~ "++" } postfix_decrement = { named_expression ~ "--" } -negation = { "-" ~ expression } +negation = { "-" ~ primary } assignment = { named_expression ~ assign_op ~ expression } +register_assignment = { register ~ assign_op ~ expression } named_expression = { - | scale - | ibase - | obase | array_item | variable_number } array_item = { letter ~ "[" ~ expression ~ "]" } +register = { + | scale + | ibase + | obase +} scale = { "scale" } ibase = { "ibase" } obase = { "obase" } diff --git a/calc/src/bc_util/instructions.rs b/calc/src/bc_util/instructions.rs index 22ab5ca0e..01a6a1458 100644 --- a/calc/src/bc_util/instructions.rs +++ b/calc/src/bc_util/instructions.rs @@ -7,14 +7,21 @@ // SPDX-License-Identifier: MIT // -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum BuiltinFunction { Length, Sqrt, Scale, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Register { + IBase, + OBase, + Scale, +} + +#[derive(Clone, Debug, PartialEq)] pub enum StmtInstruction { Break, Quit, @@ -28,6 +35,12 @@ pub enum StmtInstruction { condition: ConditionInstruction, body: Vec, }, + For { + init: ExprInstruction, + condition: ConditionInstruction, + update: ExprInstruction, + body: Vec, + }, String(String), Expr(ExprInstruction), DefineFunction { @@ -36,11 +49,8 @@ pub enum StmtInstruction { }, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum NamedExpr { - Scale, - IBase, - OBase, VariableNumber(char), ArrayItem { name: char, @@ -48,10 +58,17 @@ pub enum NamedExpr { }, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] +pub enum FunctionArgument { + Expr(ExprInstruction), + ArrayVariable(char), +} + +#[derive(Clone, Debug, PartialEq)] pub enum ExprInstruction { - Number(f64), + Number(String), Named(NamedExpr), + GetRegister(Register), Builtin { function: BuiltinFunction, arg: Box, @@ -62,10 +79,14 @@ pub enum ExprInstruction { PostDecrement(NamedExpr), Call { name: char, - args: Vec, + args: Vec, }, Assignment { - name: char, + named: NamedExpr, + value: Box, + }, + SetRegister { + register: Register, value: Box, }, UnaryMinus(Box), @@ -77,7 +98,7 @@ pub enum ExprInstruction { Pow(Box, Box), } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum ConditionInstruction { Expr(ExprInstruction), Eq(ExprInstruction, ExprInstruction), @@ -88,13 +109,13 @@ pub enum ConditionInstruction { Geq(ExprInstruction, ExprInstruction), } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Variable { Number(char), Array(char), } -#[derive(Default, Debug, PartialEq)] +#[derive(Clone, Default, Debug, PartialEq)] pub struct Function { pub name: char, pub parameters: Vec, diff --git a/calc/src/bc_util/interpreter.rs b/calc/src/bc_util/interpreter.rs new file mode 100644 index 000000000..eb7bf5da5 --- /dev/null +++ b/calc/src/bc_util/interpreter.rs @@ -0,0 +1,1054 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use std::fmt::Write; + +use crate::bc_util::instructions::Variable; + +use super::{ + instructions::{ + BuiltinFunction, ConditionInstruction, ExprInstruction, Function, FunctionArgument, + NamedExpr, Register, StmtInstruction, + }, + number::Number, + parser::Program, +}; + +pub type ExecutionResult = Result; + +type NameMap = [T; 26]; + +fn name_index(name: char) -> usize { + (name as u8 - b'a') as usize +} + +fn contains_quit(stmt: &StmtInstruction) -> bool { + match stmt { + StmtInstruction::Quit => true, + StmtInstruction::If { body, .. } => body.iter().any(contains_quit), + StmtInstruction::While { body, .. } => body.iter().any(contains_quit), + _ => false, + } +} + +fn should_print(expr: &ExprInstruction) -> bool { + // assignments should not be printed: + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/bc.html#tag_20_09_10 + !matches!( + expr, + ExprInstruction::Assignment { .. } | ExprInstruction::SetRegister { .. } + ) +} + +fn get_or_extend(array: &mut Vec, index: usize) -> &mut Number { + if index >= array.len() { + array.resize_with(index + 1, Number::zero); + } + &mut array[index] +} + +#[derive(Default)] +struct CallFrame { + variables: NameMap>, + array_variables: NameMap>>, +} + +#[derive(Debug, PartialEq)] +enum ControlFlow { + Return(Number), + Quit, + Break, + None, +} + +pub struct Interpreter { + variables: NameMap, + array_variables: NameMap>, + functions: NameMap, + call_frames: Vec, + scale: u64, + ibase: u64, + obase: u64, + output: String, +} + +impl Default for Interpreter { + fn default() -> Self { + Self { + variables: Default::default(), + array_variables: Default::default(), + functions: Default::default(), + call_frames: Vec::new(), + scale: 0, + ibase: 10, + obase: 10, + output: String::new(), + } + } +} + +pub struct Output { + pub has_quit: bool, + pub string: String, +} + +impl Interpreter { + fn output(&mut self, has_quit: bool) -> Output { + let mut string = String::new(); + std::mem::swap(&mut self.output, &mut string); + Output { has_quit, string } + } + + fn eval_named(&mut self, named: &NamedExpr) -> ExecutionResult<&mut Number> { + match named { + NamedExpr::VariableNumber(c) => { + if let Some(call_frame) = self.call_frames.last_mut() { + if let Some(value) = &mut call_frame.variables[name_index(*c)] { + return Ok(value); + } + } + Ok(&mut self.variables[name_index(*c)]) + } + NamedExpr::ArrayItem { name, index } => { + let index = self + .eval_expr(index)? + .as_u64() + .ok_or("array index is too large")? as usize; + if let Some(call_frame) = self.call_frames.last_mut() { + if let Some(array) = &mut call_frame.array_variables[name_index(*name)] { + return Ok(get_or_extend(array, index as usize)); + } + } + Ok(get_or_extend( + &mut self.array_variables[name_index(*name)], + index, + )) + } + } + } + + fn call_function(&mut self, name: char, args: &[FunctionArgument]) -> ExecutionResult { + // FIXME: remove copies + //let function = &self.functions[name_index(name)]; + let mut call_frame = CallFrame::default(); + let parameters = self.functions[name_index(name)].parameters.clone(); + + for (arg, param) in args.iter().zip(parameters.iter()) { + // check if argument and parameter match + match (arg, param) { + (FunctionArgument::Expr(expr), Variable::Number(name)) => { + let value = self.eval_expr(expr)?; + call_frame.variables[name_index(*name)] = Some(value); + } + (FunctionArgument::ArrayVariable(arg_name), Variable::Array(param_name)) => { + // arrays are passed by value + let array = self.array_variables[name_index(*arg_name)].clone(); + call_frame.array_variables[name_index(*param_name)] = Some(array) + } + _ => return Err("argument does not match parameter"), + } + } + + let function = &self.functions[name_index(name)]; + + for local in function.locals.iter() { + match local { + Variable::Number(name) => { + call_frame.variables[name_index(*name)] = Some(0.into()); + } + Variable::Array(name) => { + call_frame.array_variables[name_index(*name)] = Some(Vec::new()); + } + } + } + let body = function.body.clone(); + + self.call_frames.push(call_frame); + for stmt in &body { + match self.eval_stmt(stmt)? { + ControlFlow::Return(value) => { + self.call_frames.pop(); + return Ok(value); + } + ControlFlow::Quit => { + // this should have been handled earlier. + // A quit inside of a function definition + // should stop execution, and we can only call + // a function after its definition has been processed + panic!("reached quit in function call") + } + _ => {} + } + } + self.call_frames.pop(); + // from the POSIX standard: + // > the value of the function shall be the value of the expression + // > in the parentheses of the return statement or shall be zero + // > if no expression is provided or if there is no return statement + Ok(Number::zero()) + } + + fn eval_expr(&mut self, expr: &ExprInstruction) -> ExecutionResult { + match expr { + ExprInstruction::Number(x) => { + Number::parse(x, self.ibase).ok_or("invalid digit for the current ibase") + } + ExprInstruction::GetRegister(reg) => match reg { + Register::Scale => Ok(self.scale.into()), + Register::IBase => Ok(self.ibase.into()), + Register::OBase => Ok(self.obase.into()), + }, + ExprInstruction::Named(named) => self.eval_named(named).cloned(), + ExprInstruction::Builtin { function, arg } => match function { + BuiltinFunction::Length => Ok(self.eval_expr(arg)?.length().into()), + BuiltinFunction::Sqrt => self.eval_expr(arg)?.sqrt(self.scale), + BuiltinFunction::Scale => Ok(self.eval_expr(arg)?.scale().into()), + }, + ExprInstruction::PreIncrement(named) => { + let value = self.eval_named(named)?; + value.inc(); + Ok(value.clone()) + } + ExprInstruction::PreDecrement(named) => { + let value = self.eval_named(named)?; + value.dec(); + Ok(value.clone()) + } + ExprInstruction::PostIncrement(named) => { + let value = self.eval_named(named)?; + let result = value.clone(); + value.inc(); + Ok(result) + } + ExprInstruction::PostDecrement(named) => { + let value = self.eval_named(named)?; + let result = value.clone(); + value.dec(); + Ok(result) + } + ExprInstruction::Call { name, args } => self.call_function(*name, args), + ExprInstruction::Assignment { named, value } => { + let value = self.eval_expr(value)?; + self.eval_named(named)?.clone_from(&value); + Ok(value) + } + ExprInstruction::SetRegister { register, value } => { + // if the value is a single digit it has to be interpreted + // as an hexadecimal number, regardless of the value of ibase + let value = match value.as_ref() { + ExprInstruction::Number(n) if n.len() == 1 => { + // this cannot fail because the parser ensures that + // the value is a valid hexadecimal number + Number::parse(n, 16).unwrap() + } + _ => self.eval_expr(value)?, + }; + + match register { + Register::Scale => { + self.scale = value + .as_u64() + .ok_or("the value assigned to scale is too large")? + } + Register::IBase => { + if let Some(new_ibase) = value.as_u64() { + if (2..=16).contains(&new_ibase) { + self.ibase = new_ibase; + return Ok(value); + } + } + return Err("ibase must be between 2 and 16"); + } + Register::OBase => { + if let Some(new_obase) = value.as_u64() { + if new_obase >= 2 { + self.obase = new_obase; + } else { + return Err("obase must be greater than 1"); + } + } else { + return Err("value assigned to obase is too large"); + } + } + } + Ok(value) + } + ExprInstruction::UnaryMinus(expr) => Ok(self.eval_expr(expr)?.negate()), + ExprInstruction::Add(lhs, rhs) => Ok(self.eval_expr(lhs)?.add(&self.eval_expr(rhs)?)), + ExprInstruction::Sub(lhs, rhs) => Ok(self.eval_expr(lhs)?.sub(&self.eval_expr(rhs)?)), + ExprInstruction::Mul(lhs, rhs) => { + Ok(self.eval_expr(lhs)?.mul(&self.eval_expr(rhs)?, self.scale)) + } + ExprInstruction::Div(lhs, rhs) => Ok(self + .eval_expr(lhs)? + .div(&self.eval_expr(rhs)?, self.scale)?), + ExprInstruction::Mod(lhs, rhs) => self + .eval_expr(lhs)? + .modulus(&self.eval_expr(rhs)?, self.scale), + ExprInstruction::Pow(lhs, rhs) => { + self.eval_expr(lhs)?.pow(&self.eval_expr(rhs)?, self.scale) + } + } + } + + fn eval_condition(&mut self, condition: &ConditionInstruction) -> ExecutionResult { + match condition { + ConditionInstruction::Expr(expr) => self.eval_expr(expr).map(|val| !val.is_zero()), + ConditionInstruction::Eq(lhs, rhs) => Ok(self.eval_expr(lhs)? == self.eval_expr(rhs)?), + ConditionInstruction::Ne(lhs, rhs) => Ok(self.eval_expr(lhs)? != self.eval_expr(rhs)?), + ConditionInstruction::Lt(lhs, rhs) => Ok(self.eval_expr(lhs)? < self.eval_expr(rhs)?), + ConditionInstruction::Gt(lhs, rhs) => Ok(self.eval_expr(lhs)? > self.eval_expr(rhs)?), + ConditionInstruction::Leq(lhs, rhs) => Ok(self.eval_expr(lhs)? <= self.eval_expr(rhs)?), + ConditionInstruction::Geq(lhs, rhs) => Ok(self.eval_expr(lhs)? >= self.eval_expr(rhs)?), + } + } + + fn eval_stmt(&mut self, stmt: &StmtInstruction) -> ExecutionResult { + match stmt { + StmtInstruction::Break => { + return Ok(ControlFlow::Break); + } + StmtInstruction::Quit => { + return Ok(ControlFlow::Quit); + } + StmtInstruction::Return => { + return Ok(ControlFlow::Return(Number::zero())); + } + StmtInstruction::ReturnExpr(expr) => { + let value = self.eval_expr(expr)?; + return Ok(ControlFlow::Return(value)); + } + StmtInstruction::If { condition, body } => { + if self.eval_condition(condition)? { + for stmt in body { + let control_flow = self.eval_stmt(stmt)?; + // any control flow instruction in the body of the + // if needs to be handled by the caller + if control_flow != ControlFlow::None { + return Ok(control_flow); + } + } + } + } + StmtInstruction::While { condition, body } => { + 'while_loop: while self.eval_condition(condition)? { + for stmt in body { + let control_flow = self.eval_stmt(stmt)?; + if control_flow == ControlFlow::Break { + break 'while_loop; + } + if control_flow != ControlFlow::None { + // we either hit a return or quit + // so we need to pass that up the stack + return Ok(control_flow); + } + } + } + } + StmtInstruction::For { + init, + condition, + update, + body, + } => { + self.eval_expr(init)?; + 'for_loop: while self.eval_condition(condition)? { + for stmt in body { + let control_flow = self.eval_stmt(stmt)?; + if control_flow == ControlFlow::Break { + break 'for_loop; + } + if control_flow != ControlFlow::None { + return Ok(control_flow); + } + } + self.eval_expr(update)?; + } + } + StmtInstruction::String(s) => self.output.push_str(s), + StmtInstruction::Expr(expr) => { + let value = self.eval_expr(expr)?; + if should_print(expr) { + // this should never fail + writeln!(&mut self.output, "{}", value.to_string(self.obase)) + .expect("error appending to string"); + } + } + StmtInstruction::DefineFunction { .. } => { + // the language grammar ensures that this is never reached + panic!("function definition outside of the global scope") + } + } + Ok(ControlFlow::None) + } + + pub fn exec(&mut self, program: Program) -> ExecutionResult { + for stmt in program { + if let StmtInstruction::DefineFunction { name, function } = stmt { + // we handle this here because we need to store the function. + // doing it in eval_stmt would not work because we would need + // to clone from the reference. Since functions can only be + // defined in the global scope, this is valid. + + // first we need to check if the definition contains quit, + // in which case we need to stop execution + if function.body.iter().any(contains_quit) { + return Ok(self.output(true)); + } + + self.functions[name_index(name)] = function; + } else { + match self.eval_stmt(&stmt)? { + ControlFlow::Return(_) => { + println!("return outside of function"); + } + ControlFlow::Break => { + println!("break outside of loop"); + } + _ => {} + } + // we can't trust the return value of eval_stmt because + // unexecuted branches will not return ControlFlow::Quit, + // but we need still need to stop execution + if contains_quit(&stmt) { + return Ok(self.output(true)); + } + } + } + Ok(self.output(false)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_print_number() { + let mut interpreter = Interpreter::default(); + // ``` + // 5 + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Number( + "5".to_string(), + ))]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn print_uninitialized_variable() { + let mut interpreter = Interpreter::default(); + // ``` + // a + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Named( + NamedExpr::VariableNumber('a'), + ))]) + .unwrap(); + assert_eq!(output.string, "0\n"); + } + + #[test] + fn test_call_builtin_scale() { + let mut interpreter = Interpreter::default(); + // ``` + // scale(5) + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Builtin { + function: BuiltinFunction::Scale, + arg: Box::new(ExprInstruction::Number("5".to_string())), + })]) + .unwrap(); + assert_eq!(output.string, "0\n"); + } + + #[test] + fn test_call_builtin_sqrt() { + let mut interpreter = Interpreter::default(); + // ``` + // sqrt(25) + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Builtin { + function: BuiltinFunction::Sqrt, + arg: Box::new(ExprInstruction::Number("25".to_string())), + })]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_call_builtin_legth() { + let mut interpreter = Interpreter::default(); + // ``` + // length(5) + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Builtin { + function: BuiltinFunction::Length, + arg: Box::new(ExprInstruction::Number("5".to_string())), + })]) + .unwrap(); + assert_eq!(output.string, "1\n"); + } + + #[test] + fn test_preincrement() { + let mut interpreter = Interpreter::default(); + // ``` + // ++a + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::PreIncrement(NamedExpr::VariableNumber( + 'a', + ))), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "1\n1\n"); + } + + #[test] + fn test_predecrement() { + let mut interpreter = Interpreter::default(); + // ``` + // --a + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::PreDecrement(NamedExpr::VariableNumber( + 'a', + ))), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "-1\n-1\n"); + } + + #[test] + fn test_postincrement() { + let mut interpreter = Interpreter::default(); + // ``` + // a++ + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::PostIncrement(NamedExpr::VariableNumber( + 'a', + ))), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "0\n1\n"); + } + + #[test] + fn test_postdecrement() { + let mut interpreter = Interpreter::default(); + // ``` + // a-- + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::PostDecrement(NamedExpr::VariableNumber( + 'a', + ))), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "0\n-1\n"); + } + + #[test] + fn test_function_call() { + let mut interpreter = Interpreter::default(); + // ``` + // f() { + // 5 + // } + // f() + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![], + locals: vec![], + body: vec![StmtInstruction::Expr(ExprInstruction::Number( + "5".to_string(), + ))], + }, + }, + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![], + }), + ]) + .unwrap(); + assert_eq!(output.string, "5\n0\n"); + } + + #[test] + fn test_assignment() { + let mut interpreter = Interpreter::default(); + // ``` + // a = 5 + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("5".to_string())), + }), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_quit() { + let mut interpreter = Interpreter::default(); + // ``` + // quit + // ``` + let output = interpreter.exec(vec![StmtInstruction::Quit]).unwrap(); + assert_eq!(output.string, ""); + assert!(output.has_quit); + } + + #[test] + fn test_break_out_of_loop() { + let mut interpreter = Interpreter::default(); + // ``` + // while (1) { break; 1 } + // ``` + let output = interpreter + .exec(vec![StmtInstruction::While { + condition: ConditionInstruction::Expr(ExprInstruction::Number("1".to_string())), + body: vec![ + StmtInstruction::Break, + StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), + ], + }]) + .unwrap(); + assert_eq!(output.string, ""); + } + + #[test] + fn test_call_function_without_return() { + let mut interpreter = Interpreter::default(); + // ``` + // f() { + // } + // f() + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![], + locals: vec![], + body: vec![], + }, + }, + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![], + }), + ]) + .unwrap(); + assert_eq!(output.string, "0\n"); + } + + #[test] + fn test_call_function_with_return_expression() { + let mut interpreter = Interpreter::default(); + // ``` + // f() { + // return(5) + // } + // f() + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![], + locals: vec![], + body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Number( + "5".to_string(), + ))], + }, + }, + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![], + }), + ]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_if_true_branch() { + let mut interpreter = Interpreter::default(); + // ``` + // if (1) { + // 5 + // } + // ``` + let output = interpreter + .exec(vec![StmtInstruction::If { + condition: ConditionInstruction::Expr(ExprInstruction::Number("1".to_string())), + body: vec![StmtInstruction::Expr(ExprInstruction::Number( + "5".to_string(), + ))], + }]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_if_false_branch() { + let mut interpreter = Interpreter::default(); + // ``` + // if (0) { + // 5 + // } + // ``` + let output = interpreter + .exec(vec![StmtInstruction::If { + condition: ConditionInstruction::Expr(ExprInstruction::Number("0".to_string())), + body: vec![StmtInstruction::Expr(ExprInstruction::Number( + "5".to_string(), + ))], + }]) + .unwrap(); + assert_eq!(output.string, ""); + } + + #[test] + fn test_assignment_does_not_print() { + let mut interpreter = Interpreter::default(); + // ``` + // a = 5 + // ``` + let output = interpreter + .exec(vec![StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("5".to_string())), + })]) + .unwrap(); + assert_eq!(output.string, ""); + } + + #[test] + fn test_assign_to_array() { + let mut interpreter = Interpreter::default(); + // ``` + // a[0] = 5 + // a[0] + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + }, + value: Box::new(ExprInstruction::Number("5".to_string())), + }), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + })), + ]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_exit_after_quit_in_function_definition() { + let mut interpreter = Interpreter::default(); + // ``` + // f() { + // quit + // } + // 1 + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![], + locals: vec![], + body: vec![StmtInstruction::Quit], + }, + }, + StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), + ]) + .unwrap(); + assert_eq!(output.string, ""); + assert!(output.has_quit); + } + + #[test] + fn test_exit_after_quit_in_unexecuted_if() { + let mut interpreter = Interpreter::default(); + // ``` + // if (0) { + // 1 + // quit + // } + // 1 + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::If { + condition: ConditionInstruction::Expr(ExprInstruction::Number("0".to_string())), + body: vec![ + StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), + StmtInstruction::Quit, + ], + }, + StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), + ]) + .unwrap(); + assert_eq!(output.string, ""); + assert!(output.has_quit); + } + + #[test] + fn test_exit_after_quit_in_unexecuted_while() { + let mut interpreter = Interpreter::default(); + // ``` + // while (0) { + // 2 + // quit + // } + // 1 + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::While { + condition: ConditionInstruction::Expr(ExprInstruction::Number("0".to_string())), + body: vec![ + StmtInstruction::Expr(ExprInstruction::Number("2".to_string())), + StmtInstruction::Quit, + ], + }, + StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), + ]) + .unwrap(); + assert_eq!(output.string, ""); + assert!(output.has_quit); + } + + #[test] + fn test_assign_to_function_local_does_not_change_global() { + let mut interpreter = Interpreter::default(); + // ``` + // f() { + // auto a; + // a = 5 + // } + // f() + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![], + locals: vec![Variable::Number('a')], + body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("5".to_string())), + })], + }, + }, + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![], + }), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "0\n0\n"); + } + + #[test] + fn test_assign_to_function_parameter_does_not_change_global() { + let mut interpreter = Interpreter::default(); + // ``` + // f(a) { + // a = 5 + // } + // a = 1 + // f(a) + // a + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![Variable::Number('a')], + locals: vec![], + body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("5".to_string())), + })], + }, + }, + StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("1".to_string())), + }), + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![FunctionArgument::Expr(ExprInstruction::Named( + NamedExpr::VariableNumber('a'), + ))], + }), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + ]) + .unwrap(); + assert_eq!(output.string, "0\n1\n"); + } + + #[test] + fn test_standard_parameter_passing() { + let mut interpreter = Interpreter::default(); + // ``` + // define f(a) { + // return(a) + // } + // f(5) + //``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![Variable::Number('a')], + locals: vec![], + body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Named( + NamedExpr::VariableNumber('a'), + ))], + }, + }, + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![FunctionArgument::Expr(ExprInstruction::Number( + "5".to_string(), + ))], + }), + ]) + .unwrap(); + assert_eq!(output.string, "5\n"); + } + + #[test] + fn test_pass_arrays_by_value() { + let mut interpreter = Interpreter::default(); + // ``` + // define f(a) { + // a[0] + // a[0] = 5 + // } + // a[0] = 1 + // f(a) + // a[0] + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + parameters: vec![Variable::Array('a')], + locals: vec![], + body: vec![ + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + })), + StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + }, + value: Box::new(ExprInstruction::Number("5".to_string())), + }), + ], + }, + }, + StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + }, + value: Box::new(ExprInstruction::Number("1".to_string())), + }), + StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![FunctionArgument::ArrayVariable('a')], + }), + StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("0".to_string())), + })), + ]) + .unwrap(); + assert_eq!(output.string, "1\n0\n1\n"); + } + + #[test] + fn test_assignment_of_a_single_value_to_base_register_is_hexadecimal() { + let mut interpreter = Interpreter::default(); + // ``` + // obase = F + // obase + // ``` + let output = interpreter + .exec(vec![ + StmtInstruction::Expr(ExprInstruction::SetRegister { + register: Register::OBase, + value: Box::new(ExprInstruction::Number("F".to_string())), + }), + StmtInstruction::Expr(ExprInstruction::GetRegister(Register::OBase)), + ]) + .unwrap(); + assert_eq!(output.string, "10\n"); + } +} diff --git a/calc/src/bc_util/mod.rs b/calc/src/bc_util/mod.rs index 8ed53461c..eb1c18c32 100644 --- a/calc/src/bc_util/mod.rs +++ b/calc/src/bc_util/mod.rs @@ -8,4 +8,6 @@ // pub mod instructions; +pub mod interpreter; +mod number; pub mod parser; diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs new file mode 100644 index 000000000..0148ca030 --- /dev/null +++ b/calc/src/bc_util/number.rs @@ -0,0 +1,524 @@ +use bigdecimal::{ + num_bigint::{BigInt, Sign}, + BigDecimal, Num, One, Signed, ToPrimitive, Zero, +}; + +fn to_digit(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'A'..=b'F' => c - b'A' + 10, + _ => panic!("number has invalid digit {}", c as char), + } +} + +fn to_char(val: u8) -> char { + match val { + 0..=9 => (val + b'0') as char, + 10..=15 => (val - 10 + b'A') as char, + _ => panic!("number bigger than biggest representable base {}", val), + } +} + +fn pad_digit(d: u64, base_ilog10: u32) -> String { + let width = base_ilog10 + 1; + format!("{:0width$}", d, width = width as usize) +} + +pub type NumericResult = Result; + +#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Number(BigDecimal); + +impl Number { + fn rescale(self, new_scale: u64) -> Self { + Self(self.0.with_scale(new_scale as i64)) + } + + pub fn zero() -> Self { + Self(BigDecimal::zero()) + } + + pub fn one() -> Self { + Self(BigDecimal::one()) + } + + pub fn as_u64(&self) -> Option { + self.0.to_u64() + } + + /// Parse a number from a string in the given base. + /// # Returns + /// `None` if the string contains invalid characters for the given base. + /// # Panics + /// panics if the string is empty or if base is not in the range 2..=16. + pub fn parse(s: &str, base: u64) -> Option { + assert!(!s.is_empty(), "parsed number has no digits"); + assert!((2..=16).contains(&base), "base must be in the range 2..=16"); + + for c in s.bytes() { + if c != b'.' && to_digit(c) >= base as u8 { + return None; + } + } + + let mut integer_part = BigDecimal::zero(); + let mut fractional_part = BigDecimal::zero(); + let mut max_scale = 0; + + if let Some((int, decimal)) = s.split_once('.') { + if !int.is_empty() { + integer_part = BigInt::from_str_radix(int, base as u32).unwrap().into(); + } + max_scale = decimal.len() as u32; + let mut nominator = BigInt::zero(); + let mut denominator = BigInt::one(); + + for c in decimal.bytes() { + nominator *= base; + denominator *= base; + let digit = to_digit(c); + if digit != 0 { + nominator += digit; + } + } + fractional_part = BigDecimal::from(nominator) / BigDecimal::from(denominator); + } else { + integer_part = BigInt::from_str_radix(s, base as u32).unwrap().into(); + } + + // In regards to the scale of parsed values, the standard doesn't specify. + // The following matches the GNU implementation, which sets the scale + // to the number of fractional digits in the string, regardless of the base + Some(Self( + integer_part + fractional_part.with_scale(max_scale as i64), + )) + } + + /// Convert the number to a string in the given base. + pub fn to_string(self, base: u64) -> String { + if self.is_zero() { + return "0".to_string(); + } + if base == 10 { + return self.0.to_string(); + } + let scale = self.scale(); + let base_ilog10 = base.ilog10(); + let mut integer_part = self.0.with_scale(0); + let mut fractional_part = self.0 - &integer_part; + + let mut result = String::new(); + if integer_part.sign() == Sign::Minus { + integer_part = -integer_part; + fractional_part = -fractional_part; + result.push('-'); + } + + if integer_part.is_zero() { + result.push('0'); + } else { + let (mut integer_part, _) = integer_part.into_bigint_and_exponent(); + + let mut stack = Vec::new(); + while !integer_part.is_zero() { + let remainder = &integer_part % base; + integer_part /= base; + stack.push(remainder.to_u64().unwrap()); + } + + for digit in stack.iter().rev() { + if base <= 16 { + result.push(to_char(*digit as u8)); + } else { + result.push(' '); + result.push_str(&pad_digit(*digit, base_ilog10)) + } + } + } + + if fractional_part.is_zero() { + return result; + } + + result.push('.'); + let mut temp = BigDecimal::one(); + // TODO: what does this do? + while temp.digits() <= scale { + fractional_part *= base; + let integer_part = fractional_part.with_scale(0); + let digit = integer_part.to_u64().unwrap(); + if base <= 16 { + result.push(to_char(digit as u8)); + } else { + result.push_str(&pad_digit(digit, base_ilog10)); + result.push(' '); + } + fractional_part -= &integer_part; + temp *= base + } + // remove trailing space + if base > 16 { + result.pop(); + } + result + } + + /// The number of decimal digits in the number. + pub fn scale(&self) -> u64 { + self.0.fractional_digit_count().max(0) as u64 + } + + pub fn length(&self) -> u64 { + self.0.digits() + } + + pub fn negate(self) -> Self { + Self(-self.0) + } + + pub fn add(self, other: &Number) -> Number { + Self(self.0 + &other.0) + } + + pub fn sub(self, other: &Number) -> Number { + Self(self.0 - &other.0) + } + + pub fn mul(self, other: &Number, scale: u64) -> Number { + let a = self.scale(); + let b = other.scale(); + let required_scale = u64::min(a + b, scale.max(a).max(b)); + let result = self.0 * &other.0; + Self(result).rescale(required_scale) + } + + pub fn div(self, other: &Number, scale: u64) -> NumericResult { + if other.is_zero() { + return Err("division by zero"); + } + let result = self.0 / &other.0; + Ok(Self(result).rescale(scale)) + } + + pub fn pow(self, other: &Number, scale: u64) -> NumericResult { + if !other.0.is_integer() { + return Err("exponent has to be an integer"); + } + + let a = self.scale(); + let b = other + .0 + .to_i64() + .ok_or("exponent is too large")? + .unsigned_abs(); + let scale = if other.0.is_positive() { + u64::min(a * b, u64::max(scale, a)) + } else { + scale + }; + let mut result = BigDecimal::one(); + for _ in 0..b { + result *= &self.0; + } + if other.0.is_negative() { + result = BigDecimal::one() / result; + } + Ok(Self(result).rescale(scale)) + } + + pub fn modulus(self, other: &Number, scale: u64) -> NumericResult { + let a_over_b = self.clone().div(other, scale)?; + let scale = u64::max(scale + other.scale(), self.scale()); + let result = self.sub(&a_over_b.mul(other, scale)); + Ok(result) + } + + pub fn sqrt(self, min_scale: u64) -> NumericResult { + let scale = self.scale().max(min_scale); + if let Some(result) = self.0.sqrt() { + return Ok(Self(result).rescale(scale)); + } + Err("square root of negative number") + } + + pub fn inc(&mut self) { + self.0 += 1; + } + + pub fn dec(&mut self) { + self.0 -= 1; + } + + pub fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +impl From for Number { + fn from(n: u64) -> Self { + Self(BigDecimal::from(n)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_base_10() { + assert_eq!(&Number::from(1).to_string(10), "1"); + assert_eq!(&Number::parse("123", 10).unwrap().to_string(10), "123"); + assert_eq!( + &Number::parse("123.456", 10).unwrap().to_string(10), + "123.456" + ); + assert_eq!(&Number::parse(".1234", 10).unwrap().to_string(10), "0.1234"); + } + + #[test] + fn test_parse_base_2() { + assert_eq!(&Number::parse("1", 2).unwrap().to_string(10), "1"); + assert_eq!(&Number::parse("1101", 2).unwrap().to_string(10), "13"); + assert_eq!( + &Number::parse("1101.101", 2).unwrap().to_string(10), + "13.625" + ); + assert_eq!(&Number::parse(".1101", 2).unwrap().to_string(10), "0.8125"); + } + + #[test] + fn test_parse_base_12() { + assert_eq!(&Number::parse("1", 12).unwrap().to_string(10), "1"); + assert_eq!(&Number::parse("123", 12).unwrap().to_string(10), "171"); + assert_eq!( + &Number::parse("1B3.BA6", 12).unwrap().to_string(10), + "279.989" + ); + assert_eq!(&Number::parse(".1B3A", 12).unwrap().to_string(10), "0.1619"); + } + + #[test] + fn test_output_base_10() { + assert_eq!(Number::from(1).to_string(10), "1"); + assert_eq!(Number::parse("123", 10).unwrap().to_string(10), "123"); + assert_eq!( + Number::parse("123.456", 10).unwrap().to_string(10), + "123.456" + ); + assert_eq!(Number::parse(".1234", 10).unwrap().to_string(10), "0.1234"); + } + + #[test] + fn test_output_base_2() { + assert_eq!(Number::from(1).to_string(2), "1"); + assert_eq!(Number::from(13).to_string(2), "1101"); + assert_eq!( + Number::parse("13.625", 10).unwrap().to_string(2), + "1101.1010000000" + ); + assert_eq!( + Number::parse("0.8125", 10).unwrap().to_string(2), + "0.11010000000000" + ); + } + + #[test] + fn test_output_base_12() { + assert_eq!(Number::from(1).to_string(12), "1"); + assert_eq!(Number::from(123).negate().to_string(12), "-A3"); + assert_eq!( + Number::parse("123.321", 10).unwrap().to_string(12), + "A3.3A2" + ); + assert_eq!(Number::parse("0.0891", 10).unwrap().to_string(12), "0.109B"); + } + + #[test] + fn test_output_base_150() { + assert_eq!(Number::from(1).to_string(150), " 001"); + assert_eq!(Number::from(1040).negate().to_string(150), "- 006 140"); + assert_eq!( + Number::parse("230.461", 10).unwrap().to_string(150), + " 001 080.069 022" + ); + assert_eq!( + Number::parse("0.673", 10).unwrap().to_string(150), + "0.100 142" + ); + } + + #[test] + fn test_output_base_1029() { + assert_eq!(Number::from(1).to_string(1029), " 0001"); + assert_eq!(Number::from(1040).negate().to_string(1029), "- 0001 0011"); + assert_eq!( + Number::parse("193.286", 10).unwrap().to_string(1029), + " 0193.0294" + ); + assert_eq!( + Number::parse("0.2964", 10).unwrap().to_string(1029), + "0.0304 1024" + ); + } + + #[test] + fn test_integer_has_zero_scale() { + assert_eq!(Number::from(10).scale(), 0); + } + + #[test] + fn test_trailing_zeros_increase_scale() { + assert_eq!(Number::parse("10.000", 10).unwrap().scale(), 3); + } + + #[test] + fn test_add() { + let n = Number::parse("10.25", 10) + .unwrap() + .add(&Number::parse("20.750", 10).unwrap()); + assert_eq!(n.scale(), 3); + assert_eq!(n.to_string(10), "31.000"); + } + + #[test] + fn test_sub() { + let n = Number::parse("10.25", 10) + .unwrap() + .sub(&Number::parse("20.750", 10).unwrap()); + assert_eq!(n.scale(), 3); + assert_eq!(n.to_string(10), "-10.500"); + } + + #[test] + fn test_mul_integers() { + let n = Number::parse("2", 10) + .unwrap() + .mul(&Number::parse("5", 10).unwrap(), 10); + assert_eq!(n.scale(), 0); + assert_eq!(n.to_string(10), "10"); + } + + #[test] + fn test_mul() { + let n = Number::parse("2.25", 10).unwrap().mul(&Number::from(4), 10); + assert_eq!(n.scale(), 2); + assert_eq!(n.to_string(10), "9.00"); + } + + #[test] + fn test_div_integers_no_remainder() { + let n = Number::from(4) + .div(&Number::from(2), 0) + .expect("error dividing two positive integers"); + + assert_eq!(n.scale(), 0); + assert_eq!(n.to_string(10), "2"); + } + + #[test] + fn test_div_with_remainder_but_zero_scale_returns_an_integer() { + let n = Number::from(4) + .div(&Number::from(3), 0) + .expect("error dividing two positive integers"); + + assert_eq!(n.scale(), 0); + assert_eq!(n.to_string(10), "1"); + } + + #[test] + fn test_div_exact() { + let n = Number::parse("4.5", 10) + .unwrap() + .div(&Number::from(2), 2) + .expect("error dividing two positive integers"); + + assert_eq!(n.scale(), 2); + assert_eq!(n.to_string(10), "2.25"); + } + + #[test] + fn test_div_by_zero_is_error() { + let n = Number::parse("4.5", 10).unwrap(); + let result = n.div(&Number::zero(), 2); + assert_eq!(result, Err("division by zero")); + } + + #[test] + fn test_raise_integer_to_positive_integer() { + let n = Number::parse("2", 10) + .unwrap() + .pow(&Number::from(3), 10) + .expect("error raising 2 to the power of 3"); + assert_eq!(n.scale(), 0); + assert_eq!(n.to_string(10), "8"); + } + + #[test] + fn test_raise_integer_to_negative_integer() { + let n = Number::parse("2", 10) + .unwrap() + .pow(&Number::from(3).negate(), 2) + .expect("error raising 2 to the power of -3"); + + assert_eq!(n.scale(), 2); + assert_eq!(n.to_string(10), "0.12"); + } + + #[test] + fn test_raise_to_zero() { + let n = Number::from(2) + .pow(&Number::from(0), 0) + .expect("error raising 2 to the power of 0"); + + assert_eq!(n.scale(), 0); + assert_eq!(n.to_string(10), "1"); + } + + #[test] + fn test_raise_negative_number() { + let n = Number::from(2) + .negate() + .pow(&Number::from(2), 0) + .expect("error raising -2 to the power of 2"); + assert_eq!(n, Number::from(4)); + let n = Number::from(2) + .negate() + .pow(&Number::from(3), 0) + .expect("error raising -2 to the power of 3"); + assert_eq!(n, Number::from(8).negate()); + } + + #[test] + fn test_raise_to_non_integer_is_error() { + let n = Number::from(2); + let result = n.pow(&Number::parse("3.5", 10).unwrap(), 2); + assert_eq!(result, Err("exponent has to be an integer")); + } + + #[test] + fn test_raise_too_large_integer_is_error() { + let n = Number::from(2); + let result = n.pow( + &Number::parse("10000000000000000000000000000", 10).unwrap(), + 2, + ); + assert_eq!(result, Err("exponent is too large")); + } + + #[test] + fn test_mod_zero_is_error() { + let n = Number::parse("4.5", 10).unwrap(); + let result = n.modulus(&Number::zero(), 2); + assert_eq!(result, Err("division by zero")); + } + + #[test] + fn test_mod() { + let n = Number::from(11) + .modulus(&Number::parse("2.5", 10).unwrap(), 0) + .unwrap(); + + assert_eq!(n.scale(), 1); + assert_eq!(n.to_string(10), "1.0"); + } +} diff --git a/calc/src/bc_util/parser.rs b/calc/src/bc_util/parser.rs index c9dc8ed34..98b2179eb 100644 --- a/calc/src/bc_util/parser.rs +++ b/calc/src/bc_util/parser.rs @@ -7,7 +7,7 @@ // SPDX-License-Identifier: MIT // -use pest::{iterators::Pair, pratt_parser::PrattParser, Parser}; +use pest::{error::InputLocation, iterators::Pair, pratt_parser::PrattParser, Parser}; use super::instructions::*; @@ -47,12 +47,65 @@ fn as_variable(r: Pair) -> Variable { } } +fn to_bc_str(s: &str) -> String { + s.trim_matches('\"').to_string() +} + +fn to_bc_number(s: &str) -> String { + s.trim().replace("\\\n", "") +} + +fn as_register(r: Pair) -> Register { + match r.as_rule() { + Rule::scale => Register::Scale, + Rule::ibase => Register::IBase, + Rule::obase => Register::OBase, + _ => unreachable!(), + } +} + +fn generate_assignment( + op_rule: Rule, + named_expr: ExprInstruction, + value: ExprInstruction, + assign: F, +) -> ExprInstruction +where + F: FnOnce(Box) -> ExprInstruction, +{ + match op_rule { + Rule::assign => assign(Box::new(value)), + Rule::add_assign => assign(Box::new(ExprInstruction::Add( + Box::new(named_expr), + Box::new(value), + ))), + Rule::sub_assign => assign(Box::new(ExprInstruction::Sub( + Box::new(named_expr), + Box::new(value), + ))), + Rule::mul_assign => assign(Box::new(ExprInstruction::Mul( + Box::new(named_expr), + Box::new(value), + ))), + Rule::div_assign => assign(Box::new(ExprInstruction::Div( + Box::new(named_expr), + Box::new(value), + ))), + Rule::mod_assign => assign(Box::new(ExprInstruction::Mod( + Box::new(named_expr), + Box::new(value), + ))), + Rule::pow_assign => assign(Box::new(ExprInstruction::Pow( + Box::new(named_expr), + Box::new(value), + ))), + _ => unreachable!(), + } +} + fn parse_named_expr(expr: Pair) -> NamedExpr { let expr = first_child(expr); match expr.as_rule() { - Rule::scale => NamedExpr::Scale, - Rule::ibase => NamedExpr::IBase, - Rule::obase => NamedExpr::OBase, Rule::variable_number => NamedExpr::VariableNumber(first_char(expr.as_str())), Rule::array_item => { // name [ index ] @@ -65,11 +118,19 @@ fn parse_named_expr(expr: Pair) -> NamedExpr { } } +fn parse_function_argument(arg: Pair) -> FunctionArgument { + match arg.as_rule() { + Rule::array => FunctionArgument::ArrayVariable(as_letter(arg)), + Rule::expression => FunctionArgument::Expr(parse_expr(arg)), + _ => unreachable!(), + } +} + fn parse_primary(expr: Pair) -> ExprInstruction { let expr = expr.into_inner().next().unwrap(); match expr.as_rule() { - Rule::number => ExprInstruction::Number(expr.as_str().trim().parse().unwrap()), - Rule::paren => parse_expr(expr), + Rule::number => ExprInstruction::Number(to_bc_number(expr.as_str())), + Rule::paren => parse_expr(first_child(expr)), Rule::builtin_call => { let mut inner = expr.into_inner(); let func = match inner.next().unwrap().as_str() { @@ -88,7 +149,11 @@ fn parse_primary(expr: Pair) -> ExprInstruction { // name ( expr* ) let mut inner = expr.into_inner(); let name = as_letter(inner.next().unwrap()); - let args = inner.next().unwrap().into_inner().map(parse_expr).collect(); + let args = if let Some(args) = inner.next() { + args.into_inner().map(parse_function_argument).collect() + } else { + vec![] + }; ExprInstruction::Call { name, args } } Rule::prefix_increment => { @@ -103,65 +168,45 @@ fn parse_primary(expr: Pair) -> ExprInstruction { Rule::postfix_decrement => { ExprInstruction::PostDecrement(parse_named_expr(first_child(expr))) } - Rule::negation => ExprInstruction::UnaryMinus(Box::new(parse_expr(first_child(expr)))), + Rule::negation => ExprInstruction::UnaryMinus(Box::new(parse_primary(first_child(expr)))), + Rule::register => { + let register = match first_child(expr).as_rule() { + Rule::scale => Register::Scale, + Rule::ibase => Register::IBase, + Rule::obase => Register::OBase, + _ => unreachable!(), + }; + ExprInstruction::GetRegister(register) + } + Rule::register_assignment => { + // register assign_op expression + let mut inner = expr.into_inner(); + let register = as_register(first_child(inner.next().unwrap())); + let op = inner.next().unwrap(); + let value = parse_expr(inner.next().unwrap()); + generate_assignment( + op.as_rule(), + ExprInstruction::GetRegister(register), + value, + |value| ExprInstruction::SetRegister { register, value }, + ) + } Rule::assignment => { // name assignment_operator expr let mut inner = expr.into_inner(); - let name = as_letter(inner.next().unwrap()); + let named = parse_named_expr(inner.next().unwrap()); let op = inner.next().unwrap(); let value = parse_expr(inner.next().unwrap()); - match op.as_str() { - "=" => ExprInstruction::Assignment { - name, - value: Box::new(value), - }, - "+=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Add( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - "-=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Sub( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - "*=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Mul( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - "/=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Div( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - "%=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Mod( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - "^=" => ExprInstruction::Assignment { - name, - value: Box::new(ExprInstruction::Pow( - Box::new(ExprInstruction::Named(NamedExpr::VariableNumber(name))), - Box::new(value), - )), - }, - _ => unreachable!(), - } + generate_assignment( + op.as_rule(), + ExprInstruction::Named(named.clone()), + value, + |value| ExprInstruction::Assignment { named, value }, + ) } + Rule::named_expression => ExprInstruction::Named(parse_named_expr(expr)), - _ => unreachable!(), + r => unreachable!("found rule {:?}", r), } } @@ -207,16 +252,37 @@ fn parse_condition(expr: Pair) -> ConditionInstruction { } } -fn parse_stmt(stmt: Pair, statements: &mut Vec) { +fn parse_stmt( + stmt: Pair, + in_function: bool, + in_loop: bool, + statements: &mut Vec, +) -> Result<(), pest::error::Error> { let stmt = first_child(stmt); match stmt.as_rule() { Rule::break_stmt => { + if !in_loop { + return Err(pest::error::Error::new_from_span( + pest::error::ErrorVariant::CustomError { + message: "break outside of loop".to_string(), + }, + stmt.as_span(), + )); + } statements.push(StmtInstruction::Break); } Rule::quit => { statements.push(StmtInstruction::Quit); } Rule::return_stmt => { + if !in_function { + return Err(pest::error::Error::new_from_span( + pest::error::ErrorVariant::CustomError { + message: "return outside of function".to_string(), + }, + stmt.as_span(), + )); + } let mut inner = stmt.into_inner(); if let Some(expr) = inner.next() { statements.push(StmtInstruction::ReturnExpr(parse_expr(expr))); @@ -228,46 +294,50 @@ fn parse_stmt(stmt: Pair, statements: &mut Vec) { let mut inner = stmt.into_inner(); let condition = parse_condition(inner.next().unwrap()); let mut body = Vec::new(); - parse_stmt(inner.next().unwrap(), &mut body); + parse_stmt(inner.next().unwrap(), in_function, in_loop, &mut body)?; statements.push(StmtInstruction::If { condition, body }); } Rule::while_stmt => { + // while (condition) stmt let mut inner = stmt.into_inner(); let condition = parse_condition(inner.next().unwrap()); let mut body = Vec::new(); - parse_stmt(inner.next().unwrap(), &mut body); + parse_stmt(inner.next().unwrap(), in_function, true, &mut body)?; statements.push(StmtInstruction::While { condition, body }); } Rule::for_stmt => { + // for (init; condition; update) stmt let mut inner = stmt.into_inner(); let init = parse_expr(inner.next().unwrap()); - statements.push(StmtInstruction::Expr(init)); let condition = parse_condition(inner.next().unwrap()); - let after = parse_expr(inner.next().unwrap()); + let update = parse_expr(inner.next().unwrap()); let mut body = Vec::new(); - parse_stmt(inner.next().unwrap(), &mut body); - body.push(StmtInstruction::Expr(after)); - statements.push(StmtInstruction::While { condition, body }); + parse_stmt(inner.next().unwrap(), in_function, true, &mut body)?; + statements.push(StmtInstruction::For { + init, + condition, + update, + body, + }); } - Rule::scoped_statement_list => { + Rule::braced_statement_list => { for stmt in first_child(stmt).into_inner() { - parse_stmt(stmt, statements); + parse_stmt(stmt, in_function, in_loop, statements)?; } } Rule::string => { - statements.push(StmtInstruction::String( - stmt.as_str().trim_matches('\"').to_string(), - )); + statements.push(StmtInstruction::String(to_bc_str(stmt.as_str()))); } Rule::expression => { statements.push(StmtInstruction::Expr(parse_expr(stmt))); } _ => unreachable!(), } + Ok(()) } -fn parse_function(func: Pair) -> Function { +fn parse_function(func: Pair) -> Result> { let mut function = func.into_inner(); let name = as_letter(function.next().unwrap()); @@ -293,14 +363,14 @@ fn parse_function(func: Pair) -> Function { let mut body = Vec::new(); for stmt in statement_list.into_inner() { - parse_stmt(stmt, &mut body); + parse_stmt(stmt, true, false, &mut body)?; } - Function { + Ok(Function { name, parameters, locals, body, - } + }) } #[derive(pest_derive::Parser)] @@ -309,30 +379,59 @@ pub struct BcParser; pub type Program = Vec; +pub struct ParseError { + err: pest::error::Error, +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.err) + } +} + pub fn parse_program(text: &str) -> Result> { let program = BcParser::parse(Rule::program, text)?.next().unwrap(); let mut result = Vec::new(); - let input_items = first_child(program); - for item in input_items.into_inner() { + for item in program.into_inner() { match item.as_rule() { Rule::semicolon_list => { for stmt in item.into_inner() { - parse_stmt(stmt, &mut result); + parse_stmt(stmt, false, false, &mut result)?; } } Rule::function => { - let f = parse_function(item); + let f = parse_function(item)?; result.push(StmtInstruction::DefineFunction { name: f.name, function: f, }); } + Rule::EOI => {} _ => unreachable!(), } } Ok(result) } +fn location_end(loc: InputLocation) -> usize { + match loc { + InputLocation::Pos(p) => p, + InputLocation::Span((_, end)) => end, + } +} + +pub fn is_incomplete(text: &str) -> bool { + match parse_program(text) { + Ok(_) => false, + Err(e) => { + let pos = location_end(e.location); + pos == text.len() + || text.as_bytes()[pos..text.len().min(pos + 2)] == [b'/', b'*'] + || text.as_bytes()[pos] == b'"' + } + } +} + #[cfg(test)] mod test { use super::*; @@ -365,26 +464,32 @@ mod test { } } + #[test] + fn test_parse_empty_program() { + let instructions = parse_program("").expect("error parsing empty program"); + assert_eq!(instructions.len(), 0); + } + #[test] fn test_parse_number() { let expr = parse_expr("123\n"); - assert_eq!(expr, ExprInstruction::Number(123.0)); + assert_eq!(expr, ExprInstruction::Number("123".to_string())); let expr = parse_expr("123.456\n"); - assert_eq!(expr, ExprInstruction::Number(123.456)); + assert_eq!(expr, ExprInstruction::Number("123.456".to_string())); let expr = parse_expr(".456\n"); - assert_eq!(expr, ExprInstruction::Number(0.456)); + assert_eq!(expr, ExprInstruction::Number(".456".to_string())); let expr = parse_expr("123.\n"); - assert_eq!(expr, ExprInstruction::Number(123.0)); + assert_eq!(expr, ExprInstruction::Number("123.".to_string())); + let expr = parse_expr("1\\\n23\n"); + assert_eq!(expr, ExprInstruction::Number("123".to_string())); + let expr = parse_expr("1\\\n.23\n"); + assert_eq!(expr, ExprInstruction::Number("1.23".to_string())); + let expr = parse_expr("1.\\\n23\n"); + assert_eq!(expr, ExprInstruction::Number("1.23".to_string())); } #[test] fn test_parse_named() { - let expr = parse_expr("scale\n"); - assert_eq!(expr, ExprInstruction::Named(NamedExpr::Scale)); - let expr = parse_expr("ibase\n"); - assert_eq!(expr, ExprInstruction::Named(NamedExpr::IBase)); - let expr = parse_expr("obase\n"); - assert_eq!(expr, ExprInstruction::Named(NamedExpr::OBase)); let expr = parse_expr("a\n"); assert_eq!(expr, ExprInstruction::Named(NamedExpr::VariableNumber('a'))); let expr = parse_expr("a[1]\n"); @@ -392,11 +497,21 @@ mod test { expr, (ExprInstruction::Named(NamedExpr::ArrayItem { name: 'a', - index: Box::new(ExprInstruction::Number(1.0)) + index: Box::new(ExprInstruction::Number("1".to_string())) })) ); } + #[test] + fn test_parse_register_get() { + let expr = parse_expr("scale\n"); + assert_eq!(expr, ExprInstruction::GetRegister(Register::Scale)); + let expr = parse_expr("ibase\n"); + assert_eq!(expr, ExprInstruction::GetRegister(Register::IBase)); + let expr = parse_expr("obase\n"); + assert_eq!(expr, ExprInstruction::GetRegister(Register::OBase)); + } + #[test] fn test_parse_builtin_call() { let expr = parse_expr("length(123)\n"); @@ -404,7 +519,7 @@ mod test { expr, (ExprInstruction::Builtin { function: BuiltinFunction::Length, - arg: Box::new(ExprInstruction::Number(123.0)) + arg: Box::new(ExprInstruction::Number("123".to_string())) }) ); let expr = parse_expr("sqrt(123)\n"); @@ -412,7 +527,7 @@ mod test { expr, (ExprInstruction::Builtin { function: BuiltinFunction::Sqrt, - arg: Box::new(ExprInstruction::Number(123.0)) + arg: Box::new(ExprInstruction::Number("123".to_string())) }) ); let expr = parse_expr("scale(123)\n"); @@ -420,7 +535,7 @@ mod test { expr, (ExprInstruction::Builtin { function: BuiltinFunction::Scale, - arg: Box::new(ExprInstruction::Number(123.0)) + arg: Box::new(ExprInstruction::Number("123".to_string())) }) ); } @@ -462,39 +577,149 @@ mod test { } #[test] - fn test_parse_fn_call() { + fn test_parse_fn_call_no_args() { + let expr = parse_expr("a()\n"); + assert_eq!( + expr, + ExprInstruction::Call { + name: 'a', + args: vec![] + } + ); + } + + #[test] + fn test_parse_fn_one_arg() { let expr = parse_expr("a(1)\n"); assert_eq!( expr, ExprInstruction::Call { name: 'a', - args: vec![ExprInstruction::Number(1.0),] + args: vec![FunctionArgument::Expr(ExprInstruction::Number( + "1".to_string() + )),] + } + ); + } + + #[test] + fn test_parse_fn_multiple_args() { + let expr = parse_expr("a(1, a, b[])\n"); + assert_eq!( + expr, + ExprInstruction::Call { + name: 'a', + args: vec![ + FunctionArgument::Expr(ExprInstruction::Number("1".to_string())), + FunctionArgument::Expr(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + FunctionArgument::ArrayVariable('b') + ] + } + ); + } + + #[test] + fn test_parse_register_assignment() { + let expr = parse_expr("scale = 10\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::Scale, + value: Box::new(ExprInstruction::Number("10".to_string())) + } + ); + let expr = parse_expr("ibase = 16\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::IBase, + value: Box::new(ExprInstruction::Number("16".to_string())) + } + ); + let expr = parse_expr("obase = 2\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::OBase, + value: Box::new(ExprInstruction::Number("2".to_string())) + } + ); + } + + #[test] + fn test_parse_register_compound_assignment() { + let expr = parse_expr("scale += 10\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::Scale, + value: Box::new(ExprInstruction::Add( + Box::new(ExprInstruction::GetRegister(Register::Scale)), + Box::new(ExprInstruction::Number("10".to_string())) + )) + } + ); + let expr = parse_expr("ibase -= 16\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::IBase, + value: Box::new(ExprInstruction::Sub( + Box::new(ExprInstruction::GetRegister(Register::IBase)), + Box::new(ExprInstruction::Number("16".to_string())) + )) + } + ); + let expr = parse_expr("obase *= 2\n"); + assert_eq!( + expr, + ExprInstruction::SetRegister { + register: Register::OBase, + value: Box::new(ExprInstruction::Mul( + Box::new(ExprInstruction::GetRegister(Register::OBase)), + Box::new(ExprInstruction::Number("2".to_string())) + )) } ); } #[test] - fn test_parse_simple_assignment() { + fn test_parse_simple_variable_assignment() { let expr = parse_expr("a = 1\n"); assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', - value: Box::new(ExprInstruction::Number(1.0)) + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("1".to_string())) + }) + ); + } + + #[test] + fn test_parse_assign_to_array_element() { + let expr = parse_expr("a[20] = 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + named: NamedExpr::ArrayItem { + name: 'a', + index: Box::new(ExprInstruction::Number("20".to_string())), + }, + value: Box::new(ExprInstruction::Number("1".to_string())) }) ); } #[test] - fn test_parse_compound_assignment() { + fn test_parse_compound_variable_assignment() { let expr = parse_expr("a += 1\n"); assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', + named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Add( Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), - Box::new(ExprInstruction::Number(1.0)) + Box::new(ExprInstruction::Number("1".to_string())) )) }) ); @@ -502,10 +727,10 @@ mod test { assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', + named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Sub( Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), - Box::new(ExprInstruction::Number(1.0)) + Box::new(ExprInstruction::Number("1".to_string())) )) }) ); @@ -513,10 +738,10 @@ mod test { assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', + named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Mul( Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), - Box::new(ExprInstruction::Number(1.0)) + Box::new(ExprInstruction::Number("1".to_string())) )) }) ); @@ -524,10 +749,21 @@ mod test { assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', + named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Div( Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), - Box::new(ExprInstruction::Number(1.0)) + Box::new(ExprInstruction::Number("1".to_string())) + )) + }) + ); + let expr = parse_expr("a %= 1\n"); + assert_eq!( + expr, + (ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Mod( + Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), + Box::new(ExprInstruction::Number("1".to_string())) )) }) ); @@ -535,10 +771,10 @@ mod test { assert_eq!( expr, (ExprInstruction::Assignment { - name: 'a', + named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Pow( Box::new(ExprInstruction::Named(NamedExpr::VariableNumber('a'))), - Box::new(ExprInstruction::Number(1.0)) + Box::new(ExprInstruction::Number("1".to_string())) )) }) ); @@ -549,7 +785,7 @@ mod test { let expr = parse_expr("-1\n"); assert_eq!( expr, - ExprInstruction::UnaryMinus(Box::new(ExprInstruction::Number(1.0))) + ExprInstruction::UnaryMinus(Box::new(ExprInstruction::Number("1".to_string()))) ); } @@ -559,16 +795,16 @@ mod test { assert_eq!( expr, (ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); let expr = parse_expr("1 ^ 2\n"); assert_eq!( expr, (ExprInstruction::Pow( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); } @@ -579,10 +815,10 @@ mod test { assert_eq!( expr, ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number("1".to_string())), Box::new(ExprInstruction::Mul( - Box::new(ExprInstruction::Number(2.0)), - Box::new(ExprInstruction::Number(3.0)) + Box::new(ExprInstruction::Number("2".to_string())), + Box::new(ExprInstruction::Number("3".to_string())) )) ) ); @@ -591,23 +827,33 @@ mod test { expr, ExprInstruction::Add( Box::new(ExprInstruction::Mul( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )), - Box::new(ExprInstruction::Number(3.0)) + Box::new(ExprInstruction::Number("3".to_string())) ) ); let expr = parse_expr("1 * 2 ^ 3\n"); assert_eq!( expr, ExprInstruction::Mul( - Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number("1".to_string())), Box::new(ExprInstruction::Pow( - Box::new(ExprInstruction::Number(2.0)), - Box::new(ExprInstruction::Number(3.0)) + Box::new(ExprInstruction::Number("2".to_string())), + Box::new(ExprInstruction::Number("3".to_string())) )) ) ); + let expr = parse_expr("-1 ^ 2\n"); + assert_eq!( + expr, + ExprInstruction::Pow( + Box::new(ExprInstruction::UnaryMinus(Box::new( + ExprInstruction::Number("1".to_string()) + ))), + Box::new(ExprInstruction::Number("2".to_string())) + ) + ); } #[test] @@ -616,10 +862,10 @@ mod test { assert_eq!( expr, ExprInstruction::Pow( - Box::new(ExprInstruction::Number(1.0)), + Box::new(ExprInstruction::Number("1".to_string())), Box::new(ExprInstruction::Pow( - Box::new(ExprInstruction::Number(2.0)), - Box::new(ExprInstruction::Number(3.0)) + Box::new(ExprInstruction::Number("2".to_string())), + Box::new(ExprInstruction::Number("3".to_string())) )) ) ); @@ -627,8 +873,14 @@ mod test { #[test] fn test_parse_break() { - let stmt = parse_stmt("break\n"); - assert_eq!(stmt, StmtInstruction::Break); + let stmt = parse_stmt("while(0) break\n"); + assert_eq!( + stmt, + StmtInstruction::While { + condition: ConditionInstruction::Expr(ExprInstruction::Number("0".to_string())), + body: vec![StmtInstruction::Break] + } + ); } #[test] @@ -639,16 +891,35 @@ mod test { #[test] fn test_parse_empty_return() { - let stmt = parse_stmt("return\n"); - assert_eq!(stmt, StmtInstruction::Return); + let stmt = parse_stmt("define f() {\nreturn\n}\n"); + assert_eq!( + stmt, + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + body: vec![StmtInstruction::Return], + ..Default::default() + } + } + ); } #[test] fn test_parse_return_expr() { - let stmt = parse_stmt("return(1)\n"); + let stmt = parse_stmt("define f() {\nreturn(1)\n}\n"); assert_eq!( stmt, - StmtInstruction::ReturnExpr(ExprInstruction::Number(1.0)) + StmtInstruction::DefineFunction { + name: 'f', + function: Function { + name: 'f', + body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Number( + "1".to_string() + ))], + ..Default::default() + } + } ); } @@ -663,8 +934,8 @@ mod test { ExprInstruction::Named(NamedExpr::VariableNumber('z')) ), body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { - name: 'a', - value: Box::new(ExprInstruction::Number(2.0)) + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("2".to_string())) })] } ); @@ -681,8 +952,8 @@ mod test { ExprInstruction::Named(NamedExpr::VariableNumber('z')) ), body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { - name: 'a', - value: Box::new(ExprInstruction::Number(2.0)) + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("2".to_string())) })] } ); @@ -690,60 +961,60 @@ mod test { #[test] fn test_parse_for() { - let instructions = - parse_program("for (i = 0; i < 10; i++) a = 2\n").expect("error parsing for loop"); - assert_eq!(instructions.len(), 2); + let stmt = parse_stmt("for (i = 0; i < 10; i++) a = 2\n"); assert_eq!( - instructions[0], - StmtInstruction::Expr(ExprInstruction::Assignment { - name: 'i', - value: Box::new(ExprInstruction::Number(0.0)) - }) - ); - assert_eq!( - instructions[1], - StmtInstruction::While { + stmt, + StmtInstruction::For { + init: ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('i'), + value: Box::new(ExprInstruction::Number("0".to_string())) + }, condition: ConditionInstruction::Lt( ExprInstruction::Named(NamedExpr::VariableNumber('i')), - ExprInstruction::Number(10.0) + ExprInstruction::Number("10".to_string()) ), - body: vec![ - StmtInstruction::Expr(ExprInstruction::Assignment { - name: 'a', - value: Box::new(ExprInstruction::Number(2.0)) - }), - StmtInstruction::Expr(ExprInstruction::PostIncrement( - NamedExpr::VariableNumber('i') - )) - ] + update: ExprInstruction::PostIncrement(NamedExpr::VariableNumber('i')), + body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + named: NamedExpr::VariableNumber('a'), + value: Box::new(ExprInstruction::Number("2".to_string())) + })] } ); } #[test] - fn test_parse_empty_scoped_statement_list() { + fn test_parse_empty_braced_statement_list() { let instructions = - parse_program("{ }\n").expect("error parsing empty scoped statement list"); + parse_program("{ }\n").expect("error parsing empty braced statement list"); + assert_eq!(instructions.len(), 0); + let instructions = + parse_program("{\n}\n").expect("error parsing empty braced statement list"); + assert_eq!(instructions.len(), 0); + let instructions = + parse_program("{\n\n}\n").expect("error parsing empty braced statement list"); + assert_eq!(instructions.len(), 0); + let instructions = + parse_program("{;\n;;}\n").expect("error parsing empty braced statement list"); assert_eq!(instructions.len(), 0); } #[test] - fn test_parse_scoped_statement_list() { + fn test_parse_braced_statement_list() { let instructions = parse_program("{ 1 + 2; 3 + 4; \"string\" }\n") - .expect("error parsing scoped statement list"); + .expect("error parsing braced statement list"); assert_eq!(instructions.len(), 3); assert_eq!( instructions[0], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); assert_eq!( instructions[1], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(3.0)), - Box::new(ExprInstruction::Number(4.0)) + Box::new(ExprInstruction::Number("3".to_string())), + Box::new(ExprInstruction::Number("4".to_string())) )) ); assert_eq!( @@ -770,8 +1041,8 @@ mod test { assert_eq!( stmt, StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); } @@ -784,15 +1055,15 @@ mod test { assert_eq!( instructions[0], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); assert_eq!( instructions[1], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(3.0)), - Box::new(ExprInstruction::Number(4.0)) + Box::new(ExprInstruction::Number("3".to_string())), + Box::new(ExprInstruction::Number("4".to_string())) )) ); assert_eq!( @@ -857,8 +1128,8 @@ mod test { Function { name: 'f', body: vec![StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) ))], ..Default::default() } @@ -867,21 +1138,22 @@ mod test { #[test] fn test_ignore_comments() { - let instructions = parse_program("1 + 2; /*multiline\ncomment*/3 + 4\n") - .expect("error parsing multiple statements with comments"); + let instructions = + parse_program("/*line comment*/\n1 + 2; \n/*multiline\ncomment*/\n3 + 4\n") + .expect("error parsing multiple statements with comments"); assert_eq!(instructions.len(), 2); assert_eq!( instructions[0], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); assert_eq!( instructions[1], StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(3.0)), - Box::new(ExprInstruction::Number(4.0)) + Box::new(ExprInstruction::Number("3".to_string())), + Box::new(ExprInstruction::Number("4".to_string())) )) ); } @@ -892,9 +1164,66 @@ mod test { assert_eq!( stmt, StmtInstruction::Expr(ExprInstruction::Add( - Box::new(ExprInstruction::Number(1.0)), - Box::new(ExprInstruction::Number(2.0)) + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) )) ); } + + #[test] + fn test_break_outside_of_loop_is_an_error() { + let result = parse_program("break\n"); + assert!(result.is_err()); + } + + #[test] + fn test_return_outside_of_function_is_an_error() { + let result = parse_program("return\n"); + assert!(result.is_err()); + } + + #[test] + fn test_partial_comment_is_incomplete() { + assert!(is_incomplete("/* this is the start of a comment\n")); + assert!(is_incomplete("a + /* this is the start of a comment\n")); + assert!(is_incomplete("1 + 2;/* this is the start of a comment\n")); + } + + #[test] + fn test_partial_string_is_incomplete() { + assert!(is_incomplete("\"this is the start of a string\n")); + assert!(is_incomplete("1 + 2;\"this is the start of a string\n")); + } + + #[test] + fn test_partial_function_requires_is_incomplete() { + assert!(is_incomplete("define f() {\n")); + assert!(is_incomplete("define f() {\n auto a[], b, c, d[]\n")); + assert!(is_incomplete("define f() {\n 1 + 2\n")); + assert!(is_incomplete("define f() {\n auto a, b, c[];\n1 + 2;\n")); + } + + #[test] + fn test_unclosed_braced_statement_list_is_incomplete() { + assert!(is_incomplete("{\n")); + assert!(is_incomplete("{ 1 + 2; 3 + 4; \"string\"\n")); + } + + #[test] + fn test_statements_ending_with_a_backslash_newline_are_incomplete() { + assert!(is_incomplete("1 + 2 + \\\n")); + assert!(is_incomplete("1 + 2\\\n\\\n")); + } + + #[test] + fn test_parse_parenthesized_expression() { + let expr = parse_expr("(1 + 2)\n"); + assert_eq!( + expr, + ExprInstruction::Add( + Box::new(ExprInstruction::Number("1".to_string())), + Box::new(ExprInstruction::Number("2".to_string())) + ) + ); + } } diff --git a/calc/tests/bc/add.bc b/calc/tests/bc/add.bc new file mode 100644 index 000000000..90f782c37 --- /dev/null +++ b/calc/tests/bc/add.bc @@ -0,0 +1,5 @@ +1 + 2 +1 + 2.00 +1.00 + 2 +45.89 + 2.3 +quit diff --git a/calc/tests/bc/add.out b/calc/tests/bc/add.out new file mode 100644 index 000000000..c342bd426 --- /dev/null +++ b/calc/tests/bc/add.out @@ -0,0 +1,4 @@ +3 +3.00 +3.00 +48.19 diff --git a/calc/tests/bc/arrays_are_passed_to_function_by_value.bc b/calc/tests/bc/arrays_are_passed_to_function_by_value.bc new file mode 100644 index 000000000..c58612841 --- /dev/null +++ b/calc/tests/bc/arrays_are_passed_to_function_by_value.bc @@ -0,0 +1,13 @@ +define r(a[]) { + a[0] + a[1] + a[0] = 89.3 + a[1] = 23.7 +} + +a[0] = 1 +a[1] = 2 +r(a[]) +a[0] +a[1] +quit diff --git a/calc/tests/bc/arrays_are_passed_to_function_by_value.out b/calc/tests/bc/arrays_are_passed_to_function_by_value.out new file mode 100644 index 000000000..0e7ca13fe --- /dev/null +++ b/calc/tests/bc/arrays_are_passed_to_function_by_value.out @@ -0,0 +1,5 @@ +1 +2 +0 +1 +2 diff --git a/calc/tests/bc/assign_to_array_item.bc b/calc/tests/bc/assign_to_array_item.bc new file mode 100644 index 000000000..a746e3e16 --- /dev/null +++ b/calc/tests/bc/assign_to_array_item.bc @@ -0,0 +1,5 @@ +a[0] = 10 +a[45] = 34 +a[0] +a[45] +quit diff --git a/calc/tests/bc/assign_to_array_item.out b/calc/tests/bc/assign_to_array_item.out new file mode 100644 index 000000000..8ba133e65 --- /dev/null +++ b/calc/tests/bc/assign_to_array_item.out @@ -0,0 +1,2 @@ +10 +34 diff --git a/calc/tests/bc/assign_to_function_local_does_not_change_global.bc b/calc/tests/bc/assign_to_function_local_does_not_change_global.bc new file mode 100644 index 000000000..b72a3d903 --- /dev/null +++ b/calc/tests/bc/assign_to_function_local_does_not_change_global.bc @@ -0,0 +1,21 @@ +define t(a, b) { + auto c[], d, e + a = 1 + b = 2 + c[10] = 3 + d = 4 + e = 9 +} + +a = 10 +b = 20 +c[10] = 30 +d = 40 +e = 90 +t(1, 2) +a +b +c[10] +d +e +quit diff --git a/calc/tests/bc/assign_to_function_local_does_not_change_global.out b/calc/tests/bc/assign_to_function_local_does_not_change_global.out new file mode 100644 index 000000000..b1e7d051d --- /dev/null +++ b/calc/tests/bc/assign_to_function_local_does_not_change_global.out @@ -0,0 +1,6 @@ +0 +10 +20 +30 +40 +90 diff --git a/calc/tests/bc/assign_to_variable.bc b/calc/tests/bc/assign_to_variable.bc new file mode 100644 index 000000000..997718d7f --- /dev/null +++ b/calc/tests/bc/assign_to_variable.bc @@ -0,0 +1,5 @@ +a = 2 +b = 3 +b +a +quit diff --git a/calc/tests/bc/assign_to_variable.out b/calc/tests/bc/assign_to_variable.out new file mode 100644 index 000000000..f1e5eeed2 --- /dev/null +++ b/calc/tests/bc/assign_to_variable.out @@ -0,0 +1,2 @@ +3 +2 diff --git a/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.bc b/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.bc new file mode 100644 index 000000000..0b3bc5b90 --- /dev/null +++ b/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.bc @@ -0,0 +1,13 @@ +obase = F +obase + +ibase = 2 +ibase = D +ibase + + +ibase = 13 +ibase = F +ibase + +quit diff --git a/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.out b/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.out new file mode 100644 index 000000000..1e4021529 --- /dev/null +++ b/calc/tests/bc/assignment_of_a_single_value_to_base_register_is_hexadecimal.out @@ -0,0 +1,3 @@ +10 +D +10 diff --git a/calc/tests/bc/break.out b/calc/tests/bc/break.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/break_out_of_loop.bc b/calc/tests/bc/break_out_of_loop.bc new file mode 100644 index 000000000..c784685d3 --- /dev/null +++ b/calc/tests/bc/break_out_of_loop.bc @@ -0,0 +1,3 @@ +while(1) break +for (1; 1; 1) break +quit diff --git a/calc/tests/bc/break_out_of_loop.out b/calc/tests/bc/break_out_of_loop.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/comments.bc b/calc/tests/bc/comments.bc new file mode 100644 index 000000000..f934c66c6 --- /dev/null +++ b/calc/tests/bc/comments.bc @@ -0,0 +1,6 @@ +/* this is a comment */ + +/* this is a comment + that spans multiple lines */ + +quit diff --git a/calc/tests/bc/comments.out b/calc/tests/bc/comments.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/compound_assignment.bc b/calc/tests/bc/compound_assignment.bc new file mode 100644 index 000000000..ed72e2146 --- /dev/null +++ b/calc/tests/bc/compound_assignment.bc @@ -0,0 +1,14 @@ +a = 4 +a += 3.47 +a +a -= 9.890 +a +a *= 2 +a +a /= 1 +a +a %= 3 +a +a ^= -2 +a +quit diff --git a/calc/tests/bc/compound_assignment.out b/calc/tests/bc/compound_assignment.out new file mode 100644 index 000000000..2042cf983 --- /dev/null +++ b/calc/tests/bc/compound_assignment.out @@ -0,0 +1,6 @@ +7.47 +-2.420 +-4.840 +-4 +-1 +1 diff --git a/calc/tests/bc/define_empty_function.bc b/calc/tests/bc/define_empty_function.bc new file mode 100644 index 000000000..5661f13c2 --- /dev/null +++ b/calc/tests/bc/define_empty_function.bc @@ -0,0 +1,3 @@ +define f() { +} +quit diff --git a/calc/tests/bc/define_empty_function.out b/calc/tests/bc/define_empty_function.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/define_function_with_locals.bc b/calc/tests/bc/define_function_with_locals.bc new file mode 100644 index 000000000..e538343d4 --- /dev/null +++ b/calc/tests/bc/define_function_with_locals.bc @@ -0,0 +1,4 @@ +define h() { + auto a, b, c[] +} +quit diff --git a/calc/tests/bc/define_function_with_locals.out b/calc/tests/bc/define_function_with_locals.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/define_function_with_parameters.bc b/calc/tests/bc/define_function_with_parameters.bc new file mode 100644 index 000000000..3eb144325 --- /dev/null +++ b/calc/tests/bc/define_function_with_parameters.bc @@ -0,0 +1,3 @@ +define w(a, b, c) { +} +quit diff --git a/calc/tests/bc/define_function_with_parameters.out b/calc/tests/bc/define_function_with_parameters.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/div.bc b/calc/tests/bc/div.bc new file mode 100644 index 000000000..5d1473892 --- /dev/null +++ b/calc/tests/bc/div.bc @@ -0,0 +1,22 @@ +scale = 1 +a = 1/2 +scale(a) +a + +scale = 0 +a = 1/2 +scale(a) +a + +scale = 10 +a = 1/2 +scale(a) +a + +scale = 10 +4 / 2 +10 / 5 +3.4578 / 20 +1234 / 245.8909 + +quit diff --git a/calc/tests/bc/div.out b/calc/tests/bc/div.out new file mode 100644 index 000000000..d44836739 --- /dev/null +++ b/calc/tests/bc/div.out @@ -0,0 +1,10 @@ +1 +0.5 +0 +0 +10 +0.5000000000 +2.0000000000 +2.0000000000 +0.1728900000 +5.0184858406 diff --git a/calc/tests/bc/empty_return_returns_zero.bc b/calc/tests/bc/empty_return_returns_zero.bc new file mode 100644 index 000000000..b7bff07f9 --- /dev/null +++ b/calc/tests/bc/empty_return_returns_zero.bc @@ -0,0 +1,5 @@ +define m() { + return +} +m() +quit diff --git a/calc/tests/bc/empty_return_returns_zero.out b/calc/tests/bc/empty_return_returns_zero.out new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/calc/tests/bc/empty_return_returns_zero.out @@ -0,0 +1 @@ +0 diff --git a/calc/tests/bc/for.out b/calc/tests/bc/for.out new file mode 100644 index 000000000..8b1acc12b --- /dev/null +++ b/calc/tests/bc/for.out @@ -0,0 +1,10 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 diff --git a/calc/tests/bc/for_loop.bc b/calc/tests/bc/for_loop.bc new file mode 100644 index 000000000..3121ba59a --- /dev/null +++ b/calc/tests/bc/for_loop.bc @@ -0,0 +1,2 @@ +for (i = 0; i < 10; i++) i +quit diff --git a/calc/tests/bc/for_loop.out b/calc/tests/bc/for_loop.out new file mode 100644 index 000000000..8b1acc12b --- /dev/null +++ b/calc/tests/bc/for_loop.out @@ -0,0 +1,10 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 diff --git a/calc/tests/bc/function_returns_correct_value.bc b/calc/tests/bc/function_returns_correct_value.bc new file mode 100644 index 000000000..df878e4be --- /dev/null +++ b/calc/tests/bc/function_returns_correct_value.bc @@ -0,0 +1,6 @@ +define f() { + return(999) +} + +f() +quit diff --git a/calc/tests/bc/function_returns_correct_value.out b/calc/tests/bc/function_returns_correct_value.out new file mode 100644 index 000000000..a6905f8ba --- /dev/null +++ b/calc/tests/bc/function_returns_correct_value.out @@ -0,0 +1 @@ +999 diff --git a/calc/tests/bc/function_with_no_return_returns_zero.bc b/calc/tests/bc/function_with_no_return_returns_zero.bc new file mode 100644 index 000000000..a25c456e2 --- /dev/null +++ b/calc/tests/bc/function_with_no_return_returns_zero.bc @@ -0,0 +1,6 @@ +define f() { +} + +f() + +quit diff --git a/calc/tests/bc/function_with_no_return_returns_zero.out b/calc/tests/bc/function_with_no_return_returns_zero.out new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/calc/tests/bc/function_with_no_return_returns_zero.out @@ -0,0 +1 @@ +0 diff --git a/calc/tests/bc/if.bc b/calc/tests/bc/if.bc new file mode 100644 index 000000000..04b16bb3a --- /dev/null +++ b/calc/tests/bc/if.bc @@ -0,0 +1,5 @@ +if (1) 2 +if (0) 3 +if (0 < 2) 4 +if (0 != 2) 5 +quit diff --git a/calc/tests/bc/if.out b/calc/tests/bc/if.out new file mode 100644 index 000000000..0429fae9e --- /dev/null +++ b/calc/tests/bc/if.out @@ -0,0 +1,3 @@ +2 +4 +5 diff --git a/calc/tests/bc/length.bc b/calc/tests/bc/length.bc new file mode 100644 index 000000000..a4e591ad4 --- /dev/null +++ b/calc/tests/bc/length.bc @@ -0,0 +1,6 @@ +length(0) +length(1) +length(12435) +length(1234.567890) +length(0.3457) +quit diff --git a/calc/tests/bc/length.out b/calc/tests/bc/length.out new file mode 100644 index 000000000..f544c1450 --- /dev/null +++ b/calc/tests/bc/length.out @@ -0,0 +1,5 @@ +1 +1 +5 +10 +4 diff --git a/calc/tests/bc/mod.bc b/calc/tests/bc/mod.bc new file mode 100644 index 000000000..c0044f637 --- /dev/null +++ b/calc/tests/bc/mod.bc @@ -0,0 +1,24 @@ +scale = 0 +a = 4 % 2 +scale(a) +a + +scale = 1 +a = 4.000 % 2 +scale(a) +a + +scale = 4 +a = 4 % 2 +scale(a) +a + +scale = 2 +a = 4.000 % 2.00 +scale(a) +a + +scale = 0 +23.234 % 7.8902 + +quit diff --git a/calc/tests/bc/mod.out b/calc/tests/bc/mod.out new file mode 100644 index 000000000..06f61241b --- /dev/null +++ b/calc/tests/bc/mod.out @@ -0,0 +1,9 @@ +0 +0 +3 +0 +4 +0 +4 +0 +7.4536 diff --git a/calc/tests/bc/mul.bc b/calc/tests/bc/mul.bc new file mode 100644 index 000000000..2929f243d --- /dev/null +++ b/calc/tests/bc/mul.bc @@ -0,0 +1,28 @@ +a = 2 * 2 +scale(a) +a + +scale = 2 +a = 2.000 * 2.000 +scale(a) +a + +scale = 10 +a = 2.000 * 2.000 +scale(a) +a + +scale = 10 +a = 2.000 * 2.0 +scale(a) +a + +scale = 10 +a = 2.0 * 2.000 +scale(a) +a + +scale = 100 +890901.1340 * 234.93124012384009 + +quit diff --git a/calc/tests/bc/mul.out b/calc/tests/bc/mul.out new file mode 100644 index 000000000..2cbf67f2a --- /dev/null +++ b/calc/tests/bc/mul.out @@ -0,0 +1,11 @@ +0 +4 +3 +4.000 +6 +4.000000 +4 +4.0000 +4 +4.0000 +209300508.238355436615662060 diff --git a/calc/tests/bc/multiline_numbers.bc b/calc/tests/bc/multiline_numbers.bc new file mode 100644 index 000000000..511815d1a --- /dev/null +++ b/calc/tests/bc/multiline_numbers.bc @@ -0,0 +1,13 @@ +1\ +2 + +123\ +456 + +0.\ +345 + +0\ +.456 + +quit diff --git a/calc/tests/bc/multiline_numbers.out b/calc/tests/bc/multiline_numbers.out new file mode 100644 index 000000000..86a134020 --- /dev/null +++ b/calc/tests/bc/multiline_numbers.out @@ -0,0 +1,4 @@ +12 +123456 +0.345 +0.456 diff --git a/calc/tests/bc/operator_precedence.bc b/calc/tests/bc/operator_precedence.bc new file mode 100644 index 000000000..41e435c6f --- /dev/null +++ b/calc/tests/bc/operator_precedence.bc @@ -0,0 +1,19 @@ +a = 3.5 +b = 9.78 +c = 4 + +++c * d +-a ^ 2 +a * b + c +b - a / c +a * b / c + a +c ^ 2 * a +(a + b) * c +((a + b) * c - a) / b +2 + a += b + +if (a + b == b + a) { + "if works" +} + +quit diff --git a/calc/tests/bc/operator_precedence.out b/calc/tests/bc/operator_precedence.out new file mode 100644 index 000000000..dba5f2ff0 --- /dev/null +++ b/calc/tests/bc/operator_precedence.out @@ -0,0 +1,10 @@ +0 +12.2 +39.23 +9.78 +9.5 +87.5 +66.40 +6 +15.28 +if works \ No newline at end of file diff --git a/calc/tests/bc/output_base_1097.bc b/calc/tests/bc/output_base_1097.bc new file mode 100644 index 000000000..dc6bf3168 --- /dev/null +++ b/calc/tests/bc/output_base_1097.bc @@ -0,0 +1,6 @@ +obase = 1097 +0 +1 +10783 +103072.2109348 +quit diff --git a/calc/tests/bc/output_base_1097.out b/calc/tests/bc/output_base_1097.out new file mode 100644 index 000000000..7119b2328 --- /dev/null +++ b/calc/tests/bc/output_base_1097.out @@ -0,0 +1,4 @@ +0 + 0001 + 0009 0910 + 0093 1051.0231 0433 0917 diff --git a/calc/tests/bc/output_base_14.bc b/calc/tests/bc/output_base_14.bc new file mode 100644 index 000000000..e17e9f72d --- /dev/null +++ b/calc/tests/bc/output_base_14.bc @@ -0,0 +1,6 @@ +obase = 14 +0 +1 +123.790 +0.7893 +quit diff --git a/calc/tests/bc/output_base_14.out b/calc/tests/bc/output_base_14.out new file mode 100644 index 000000000..6c1c70585 --- /dev/null +++ b/calc/tests/bc/output_base_14.out @@ -0,0 +1,4 @@ +0 +1 +8B.B0B +0.B09B diff --git a/calc/tests/bc/output_base_6.bc b/calc/tests/bc/output_base_6.bc new file mode 100644 index 000000000..9ef29d8ec --- /dev/null +++ b/calc/tests/bc/output_base_6.bc @@ -0,0 +1,6 @@ +obase = 6 +0 +1 +1034 +238.672 +quit diff --git a/calc/tests/bc/output_base_6.out b/calc/tests/bc/output_base_6.out new file mode 100644 index 000000000..bd65cd6a6 --- /dev/null +++ b/calc/tests/bc/output_base_6.out @@ -0,0 +1,4 @@ +0 +1 +4442 +1034.4010 diff --git a/calc/tests/bc/output_base_67.bc b/calc/tests/bc/output_base_67.bc new file mode 100644 index 000000000..8cd8eac45 --- /dev/null +++ b/calc/tests/bc/output_base_67.bc @@ -0,0 +1,6 @@ +obase = 67 +0 +1 +1240 +128.906662 +quit diff --git a/calc/tests/bc/output_base_67.out b/calc/tests/bc/output_base_67.out new file mode 100644 index 000000000..9314dcce3 --- /dev/null +++ b/calc/tests/bc/output_base_67.out @@ -0,0 +1,4 @@ +0 + 01 + 18 34 + 01 61.60 50 00 25 diff --git a/calc/tests/bc/postfix_decrement.bc b/calc/tests/bc/postfix_decrement.bc new file mode 100644 index 000000000..b2ba93501 --- /dev/null +++ b/calc/tests/bc/postfix_decrement.bc @@ -0,0 +1,3 @@ +o = 234.0 +--o +quit diff --git a/calc/tests/bc/postfix_decrement.out b/calc/tests/bc/postfix_decrement.out new file mode 100644 index 000000000..f571e9d7b --- /dev/null +++ b/calc/tests/bc/postfix_decrement.out @@ -0,0 +1 @@ +233.0 diff --git a/calc/tests/bc/postfix_increment.bc b/calc/tests/bc/postfix_increment.bc new file mode 100644 index 000000000..002c0e4b3 --- /dev/null +++ b/calc/tests/bc/postfix_increment.bc @@ -0,0 +1,3 @@ +d = 10 +d++ +quit diff --git a/calc/tests/bc/postfix_increment.out b/calc/tests/bc/postfix_increment.out new file mode 100644 index 000000000..f599e28b8 --- /dev/null +++ b/calc/tests/bc/postfix_increment.out @@ -0,0 +1 @@ +10 diff --git a/calc/tests/bc/pow.bc b/calc/tests/bc/pow.bc new file mode 100644 index 000000000..7f0fe7243 --- /dev/null +++ b/calc/tests/bc/pow.bc @@ -0,0 +1,44 @@ +scale = 2 +a = 2.000 ^ 10 +scale(a) +a + +scale = 4 +a = 2.000 ^ 10 +scale(a) +a + +scale = 4 +a = 2.000 ^ 1 +scale(a) +a + +scale = 0 +a = 2 ^ -1 +scale(a) +a + +a = 2.00 ^ -1 +scale(a) +a + +scale = 2 +a = 2.00 ^ -1 +scale(a) +a + +scale = 10 +2 ^ -1 +2 ^ -2 +89.290 ^ -1 + +scale = 0 +2 ^ 1 +2 ^ 2 +3.489 ^ 4 +10 ^ 10 +0 ^ 0 +0 ^ 38 +1 ^ 1 +1 ^ 30 +quit diff --git a/calc/tests/bc/pow.out b/calc/tests/bc/pow.out new file mode 100644 index 000000000..48ae83234 --- /dev/null +++ b/calc/tests/bc/pow.out @@ -0,0 +1,23 @@ +3 +1024.000 +4 +1024.0000 +3 +2.000 +0 +0 +0 +0 +2 +0.50 +0.5000000000 +0.2500000000 +0.0111994624 +2 +4 +148.184 +10000000000 +1 +0 +1 +1 diff --git a/calc/tests/bc/prefix_decrement.bc b/calc/tests/bc/prefix_decrement.bc new file mode 100644 index 000000000..1a21c909c --- /dev/null +++ b/calc/tests/bc/prefix_decrement.bc @@ -0,0 +1,3 @@ +a = 340.12234 +--a +quit diff --git a/calc/tests/bc/prefix_decrement.out b/calc/tests/bc/prefix_decrement.out new file mode 100644 index 000000000..ac9989bf2 --- /dev/null +++ b/calc/tests/bc/prefix_decrement.out @@ -0,0 +1 @@ +339.12234 diff --git a/calc/tests/bc/prefix_increment.bc b/calc/tests/bc/prefix_increment.bc new file mode 100644 index 000000000..d726c275d --- /dev/null +++ b/calc/tests/bc/prefix_increment.bc @@ -0,0 +1,3 @@ +a = 3.8912 +++a +quit diff --git a/calc/tests/bc/prefix_increment.out b/calc/tests/bc/prefix_increment.out new file mode 100644 index 000000000..680c3976b --- /dev/null +++ b/calc/tests/bc/prefix_increment.out @@ -0,0 +1 @@ +4.8912 diff --git a/calc/tests/bc/quit.bc b/calc/tests/bc/quit.bc new file mode 100644 index 000000000..ff604669b --- /dev/null +++ b/calc/tests/bc/quit.bc @@ -0,0 +1 @@ +quit diff --git a/calc/tests/bc/quit.out b/calc/tests/bc/quit.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/quit_in_unexecuted_code.bc b/calc/tests/bc/quit_in_unexecuted_code.bc new file mode 100644 index 000000000..3382e9648 --- /dev/null +++ b/calc/tests/bc/quit_in_unexecuted_code.bc @@ -0,0 +1,9 @@ +define x() { + quit +} +if (0) quit +while (0) quit +for (0; 0; 0) quit +1 +2 +3 diff --git a/calc/tests/bc/quit_in_unexecuted_code.out b/calc/tests/bc/quit_in_unexecuted_code.out new file mode 100644 index 000000000..e69de29bb diff --git a/calc/tests/bc/read_base_10.bc b/calc/tests/bc/read_base_10.bc new file mode 100644 index 000000000..ee8f36704 --- /dev/null +++ b/calc/tests/bc/read_base_10.bc @@ -0,0 +1,8 @@ +0 +1 +130 +0.234 +.388 +2011.0367 +-3478.89 +quit diff --git a/calc/tests/bc/read_base_10.out b/calc/tests/bc/read_base_10.out new file mode 100644 index 000000000..8fea7dffe --- /dev/null +++ b/calc/tests/bc/read_base_10.out @@ -0,0 +1,7 @@ +0 +1 +130 +0.234 +0.388 +2011.0367 +-3478.89 diff --git a/calc/tests/bc/read_base_15.bc b/calc/tests/bc/read_base_15.bc new file mode 100644 index 000000000..311fadbb3 --- /dev/null +++ b/calc/tests/bc/read_base_15.bc @@ -0,0 +1,6 @@ +ibase = 15 +0 +1 +ACDC2 +123BAD.4DA +quit diff --git a/calc/tests/bc/read_base_15.out b/calc/tests/bc/read_base_15.out new file mode 100644 index 000000000..ba0177e73 --- /dev/null +++ b/calc/tests/bc/read_base_15.out @@ -0,0 +1,4 @@ +0 +1 +549857 +873388.327 diff --git a/calc/tests/bc/read_base_2.bc b/calc/tests/bc/read_base_2.bc new file mode 100644 index 000000000..71c73ede9 --- /dev/null +++ b/calc/tests/bc/read_base_2.bc @@ -0,0 +1,6 @@ +ibase = 2 +0 +1 +0110110100001 +1011110.0001001 +quit diff --git a/calc/tests/bc/read_base_2.out b/calc/tests/bc/read_base_2.out new file mode 100644 index 000000000..612ecd8a9 --- /dev/null +++ b/calc/tests/bc/read_base_2.out @@ -0,0 +1,4 @@ +0 +1 +3489 +94.0703125 diff --git a/calc/tests/bc/scale.bc b/calc/tests/bc/scale.bc new file mode 100644 index 000000000..596f383d6 --- /dev/null +++ b/calc/tests/bc/scale.bc @@ -0,0 +1,6 @@ +scale(0) +scale(1) +scale(12430) +scale(2.000) +scale(234345.9032490) +quit diff --git a/calc/tests/bc/scale.out b/calc/tests/bc/scale.out new file mode 100644 index 000000000..e6e4c460c --- /dev/null +++ b/calc/tests/bc/scale.out @@ -0,0 +1,5 @@ +0 +0 +0 +3 +7 diff --git a/calc/tests/bc/sqrt.bc b/calc/tests/bc/sqrt.bc new file mode 100644 index 000000000..37497262c --- /dev/null +++ b/calc/tests/bc/sqrt.bc @@ -0,0 +1,30 @@ +a = sqrt(0) +scale(a) +a + +a = sqrt(1) +scale(a) +a + +a = sqrt(2) +scale(a) +a + +scale = 10 +a = sqrt(2) +scale(a) +a + +scale = 0 +a = sqrt(2.00000) +scale(a) +a + +a = sqrt(4) +scale(a) +a + +a = sqrt(0.25) +scale(a) +a +quit diff --git a/calc/tests/bc/sqrt.out b/calc/tests/bc/sqrt.out new file mode 100644 index 000000000..f968d236d --- /dev/null +++ b/calc/tests/bc/sqrt.out @@ -0,0 +1,14 @@ +0 +0 +0 +1 +0 +1 +10 +1.4142135623 +5 +1.41421 +0 +2 +2 +0.50 diff --git a/calc/tests/bc/strings.bc b/calc/tests/bc/strings.bc new file mode 100644 index 000000000..bae3bc1b6 --- /dev/null +++ b/calc/tests/bc/strings.bc @@ -0,0 +1,4 @@ +"line string" +"multiline\ +string" +quit diff --git a/calc/tests/bc/strings.out b/calc/tests/bc/strings.out new file mode 100644 index 000000000..355d0ac82 --- /dev/null +++ b/calc/tests/bc/strings.out @@ -0,0 +1,2 @@ +line stringmultiline\ +string \ No newline at end of file diff --git a/calc/tests/bc/sub.bc b/calc/tests/bc/sub.bc new file mode 100644 index 000000000..d4913aa8c --- /dev/null +++ b/calc/tests/bc/sub.bc @@ -0,0 +1,5 @@ +1 - 2 +1 - 2.00 +1.00 - 2 +45.89 - 2.3 +quit diff --git a/calc/tests/bc/sub.out b/calc/tests/bc/sub.out new file mode 100644 index 000000000..c69f86e3f --- /dev/null +++ b/calc/tests/bc/sub.out @@ -0,0 +1,4 @@ +-1 +-1.00 +-1.00 +43.59 diff --git a/calc/tests/bc/unary_minus.bc b/calc/tests/bc/unary_minus.bc new file mode 100644 index 000000000..c9db7bac9 --- /dev/null +++ b/calc/tests/bc/unary_minus.bc @@ -0,0 +1,8 @@ +-0 +-1 +a = 20 +-a +a = -20 +-a +- - 2.45 +quit diff --git a/calc/tests/bc/unary_minus.out b/calc/tests/bc/unary_minus.out new file mode 100644 index 000000000..9ced5cbb2 --- /dev/null +++ b/calc/tests/bc/unary_minus.out @@ -0,0 +1,5 @@ +0 +-1 +-20 +20 +2.45 diff --git a/calc/tests/bc/uninitialized_variables_are_zero.bc b/calc/tests/bc/uninitialized_variables_are_zero.bc new file mode 100644 index 000000000..77723e0d8 --- /dev/null +++ b/calc/tests/bc/uninitialized_variables_are_zero.bc @@ -0,0 +1,2 @@ +a +quit diff --git a/calc/tests/bc/uninitialized_variables_are_zero.out b/calc/tests/bc/uninitialized_variables_are_zero.out new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/calc/tests/bc/uninitialized_variables_are_zero.out @@ -0,0 +1 @@ +0 diff --git a/calc/tests/bc/while.out b/calc/tests/bc/while.out new file mode 100644 index 000000000..f00c965d8 --- /dev/null +++ b/calc/tests/bc/while.out @@ -0,0 +1,10 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 diff --git a/calc/tests/bc/while_loop.bc b/calc/tests/bc/while_loop.bc new file mode 100644 index 000000000..3e0e70de4 --- /dev/null +++ b/calc/tests/bc/while_loop.bc @@ -0,0 +1,3 @@ +i = 0 +while(i < 10) ++i +quit diff --git a/calc/tests/bc/while_loop.out b/calc/tests/bc/while_loop.out new file mode 100644 index 000000000..f00c965d8 --- /dev/null +++ b/calc/tests/bc/while_loop.out @@ -0,0 +1,10 @@ +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 diff --git a/calc/tests/integration.rs b/calc/tests/integration.rs index 29a35cea9..1088345f8 100644 --- a/calc/tests/integration.rs +++ b/calc/tests/integration.rs @@ -22,6 +22,26 @@ fn expr_test(args: &[&str], expected_output: &str) { }); } +fn test_bc(program: &str, expected_output: &str) { + run_test(TestPlan { + cmd: String::from("bc"), + args: vec![], + stdin_data: program.to_string(), + expected_out: String::from(expected_output), + expected_err: String::from(""), + expected_exit_code: 0, + }); +} + +macro_rules! test_bc { + ($test_name:ident) => { + test_bc( + include_str!(concat!("bc/", stringify!($test_name), ".bc")), + include_str!(concat!("bc/", stringify!($test_name), ".out")), + ) + }; +} + #[test] fn test_expr_logops() { expr_test(&["4", "|", "5", "+", "1"], "5\n"); @@ -59,3 +79,223 @@ fn test_expr_cmpstr() { expr_test(&["aaa", "=", "bbb", "+", "1"], "1\n"); expr_test(&["aaa", "!=", "bbb", "+", "1"], "2\n"); } + +#[test] +fn test_bc_add() { + test_bc!(add) +} + +#[test] +fn test_bc_arrays_are_passed_to_function_by_value() { + test_bc!(arrays_are_passed_to_function_by_value) +} + +#[test] +fn test_bc_assignment_of_a_single_value_to_base_register_is_hexadecimal() { + test_bc!(assignment_of_a_single_value_to_base_register_is_hexadecimal) +} + +#[test] +fn test_bc_assign_to_array_item() { + test_bc!(assign_to_array_item) +} + +#[test] +fn test_bc_assign_to_function_local_does_not_change_global() { + test_bc!(assign_to_function_local_does_not_change_global) +} + +#[test] +fn test_bc_assign_to_variable() { + test_bc!(assign_to_variable) +} + +#[test] +fn test_bc_break_out_of_loop() { + test_bc!(break_out_of_loop) +} + +#[test] +fn test_bc_comments() { + test_bc!(comments) +} + +#[test] +fn test_bc_compound_assignment() { + test_bc!(compound_assignment) +} + +#[test] +fn test_bc_define_empty_function() { + test_bc!(define_empty_function) +} + +#[test] +fn test_bc_define_function_with_locals() { + test_bc!(define_function_with_locals) +} + +#[test] +fn test_bc_define_function_with_parameters() { + test_bc!(define_function_with_parameters) +} + +#[test] +fn test_bc_div() { + test_bc!(div) +} + +#[test] +fn test_bc_empty_return_returns_zero() { + test_bc!(empty_return_returns_zero) +} + +#[test] +fn test_bc_for_loop() { + test_bc!(for_loop) +} + +#[test] +fn test_bc_function_returns_correct_value() { + test_bc!(function_returns_correct_value) +} + +#[test] +fn test_bc_function_with_no_return_returns_zero() { + test_bc!(function_with_no_return_returns_zero) +} + +#[test] +fn test_bc_if() { + test_bc!(if) +} + +#[test] +fn test_bc_length() { + test_bc!(length) +} + +#[test] +fn test_bc_mod() { + test_bc!(mod) +} + +#[test] +fn test_bc_mul() { + test_bc!(mul) +} + +#[test] +fn test_bc_multiline_numbers() { + test_bc!(multiline_numbers) +} + +#[test] +fn test_bc_operator_precedence() { + test_bc!(operator_precedence) +} + +#[test] +fn test_bc_output_base_1097() { + test_bc!(output_base_1097) +} + +#[test] +fn test_bc_output_base_14() { + test_bc!(output_base_14) +} + +#[test] +fn test_bc_output_base_67() { + test_bc!(output_base_67) +} + +#[test] +fn test_bc_output_base_6() { + test_bc!(output_base_6) +} + +#[test] +fn test_bc_postfix_decrement() { + test_bc!(postfix_decrement) +} + +#[test] +fn test_bc_postfix_increment() { + test_bc!(postfix_increment) +} + +#[test] +fn test_bc_pow() { + test_bc!(pow) +} + +#[test] +fn test_bc_prefix_decrement() { + test_bc!(prefix_decrement) +} + +#[test] +fn test_bc_prefix_increment() { + test_bc!(prefix_increment) +} + +#[test] +fn test_bc_quit() { + test_bc!(quit) +} + +#[test] +fn test_bc_quit_in_unexecuted_code() { + test_bc!(quit_in_unexecuted_code) +} + +#[test] +fn test_bc_read_base_10() { + test_bc!(read_base_10) +} + +#[test] +fn test_bc_read_base_15() { + test_bc!(read_base_15) +} + +#[test] +fn test_bc_read_base_2() { + test_bc!(read_base_2) +} + +#[test] +fn test_bc_scale() { + test_bc!(scale) +} + +#[test] +fn test_bc_sqrt() { + test_bc!(sqrt) +} + +#[test] +fn test_bc_strings() { + test_bc!(strings) +} + +#[test] +fn test_bc_sub() { + test_bc!(sub) +} + +#[test] +fn test_bc_unary_minus() { + test_bc!(unary_minus) +} + +#[test] +fn test_bc_uninitialized_variables_are_zero() { + test_bc!(uninitialized_variables_are_zero) +} + +#[test] +fn test_bc_while_loop() { + test_bc!(while_loop) +} From 3512515e0c1a08018231915a956afa97e6a320ef Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 16:28:39 +0200 Subject: [PATCH 03/15] bc: improve repl with rustyline --- Cargo.lock | 138 ++++++++++++++++++++++++++++++++++++++++++++++++ calc/Cargo.toml | 1 + calc/src/bc.rs | 45 ++++++++++++---- 3 files changed, 174 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d16a0398d..a0006f6fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.38" @@ -234,6 +240,15 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clipboard-win" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +dependencies = [ + "error-code", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -351,6 +366,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "equivalent" version = "1.0.1" @@ -376,6 +397,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "flate2" version = "1.0.30" @@ -457,6 +495,15 @@ dependencies = [ "libc", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -550,6 +597,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "locale_config" version = "0.3.0" @@ -605,6 +658,27 @@ dependencies = [ "adler", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -815,6 +889,7 @@ dependencies = [ "pest_derive", "plib", "regex", + "rustyline", ] [[package]] @@ -996,6 +1071,16 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -1051,6 +1136,41 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.52.0", +] + [[package]] name = "ruzstd" version = "0.6.0" @@ -1099,6 +1219,12 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1270,6 +1396,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/calc/Cargo.toml b/calc/Cargo.toml index b49c51211..e787d50ff 100644 --- a/calc/Cargo.toml +++ b/calc/Cargo.toml @@ -12,6 +12,7 @@ pest = "2.7.10" pest_derive = "2.7.10" lazy_static = "1.4.0" bigdecimal = "0.4.3" +rustyline = "14.0.0" [[bin]] name = "expr" diff --git a/calc/src/bc.rs b/calc/src/bc.rs index d054a1ab0..00cb3bee4 100644 --- a/calc/src/bc.rs +++ b/calc/src/bc.rs @@ -15,6 +15,8 @@ use bc_util::{ }; use clap::Parser; +use rustyline::{error::ReadlineError, DefaultEditor, Result}; + mod bc_util; /// bc - arbitrary-precision arithmetic language @@ -47,33 +49,56 @@ fn exec_str(s: &str, interpreter: &mut Interpreter) -> bool { false } -fn main() { +fn main() -> Result<()> { let args = Args::parse(); let mut interpreter = Interpreter::default(); for file in args.files { match std::fs::read_to_string(&file) { Ok(s) => { if exec_str(&s, &mut interpreter) { - return; + return Ok(()); } } Err(_) => { eprintln!("Could not read file: {}", file.to_string_lossy()); - return; + return Ok(()); } }; } - let mut buf = String::new(); + let mut repl = DefaultEditor::new()?; + let mut line_buffer = String::new(); loop { - std::io::stdin().read_line(&mut buf).unwrap(); - if is_incomplete(&buf) { - continue; + let line = if line_buffer.is_empty() { + repl.readline(">> ") } else { - if exec_str(&buf, &mut interpreter) { - return; + repl.readline(".. ") + }; + match line { + Ok(line) => { + line_buffer.push_str(&line); + line_buffer.push('\n'); + if !is_incomplete(&line_buffer) { + if exec_str(&line_buffer, &mut interpreter) { + return Ok(()); + } + line_buffer.clear(); + } + repl.add_history_entry(line)?; + } + Err(ReadlineError::Eof) => { + println!("CTRL-D"); + break; + } + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); + break; + } + Err(e) => { + eprintln!("Error: {:?}", e); + break; } - buf.clear() } } + Ok(()) } From 9de1f044c57aef686fef5bf98e63952be319c6aa Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 21:40:08 +0200 Subject: [PATCH 04/15] bc: use Rc to make functions cheap to copy --- calc/src/bc_util/instructions.rs | 21 +++++++-- calc/src/bc_util/interpreter.rs | 74 ++++++++++++++++---------------- calc/src/bc_util/parser.rs | 28 ++++++------ 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/calc/src/bc_util/instructions.rs b/calc/src/bc_util/instructions.rs index 01a6a1458..23e91a56f 100644 --- a/calc/src/bc_util/instructions.rs +++ b/calc/src/bc_util/instructions.rs @@ -7,6 +7,8 @@ // SPDX-License-Identifier: MIT // +use std::rc::Rc; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum BuiltinFunction { Length, @@ -115,10 +117,21 @@ pub enum Variable { Array(char), } -#[derive(Clone, Default, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Function { pub name: char, - pub parameters: Vec, - pub locals: Vec, - pub body: Vec, + pub parameters: Rc<[Variable]>, + pub locals: Rc<[Variable]>, + pub body: Rc<[StmtInstruction]>, +} + +impl Default for Function { + fn default() -> Self { + Function { + name: '\0', + parameters: Rc::new([]), + locals: Rc::new([]), + body: Rc::new([]), + } + } } diff --git a/calc/src/bc_util/interpreter.rs b/calc/src/bc_util/interpreter.rs index eb7bf5da5..f9e19c1da 100644 --- a/calc/src/bc_util/interpreter.rs +++ b/calc/src/bc_util/interpreter.rs @@ -134,12 +134,10 @@ impl Interpreter { } fn call_function(&mut self, name: char, args: &[FunctionArgument]) -> ExecutionResult { - // FIXME: remove copies - //let function = &self.functions[name_index(name)]; + let function = &self.functions[name_index(name)].clone(); let mut call_frame = CallFrame::default(); - let parameters = self.functions[name_index(name)].parameters.clone(); - for (arg, param) in args.iter().zip(parameters.iter()) { + for (arg, param) in args.iter().zip(function.parameters.iter()) { // check if argument and parameter match match (arg, param) { (FunctionArgument::Expr(expr), Variable::Number(name)) => { @@ -155,8 +153,6 @@ impl Interpreter { } } - let function = &self.functions[name_index(name)]; - for local in function.locals.iter() { match local { Variable::Number(name) => { @@ -170,7 +166,7 @@ impl Interpreter { let body = function.body.clone(); self.call_frames.push(call_frame); - for stmt in &body { + for stmt in body.iter() { match self.eval_stmt(stmt)? { ControlFlow::Return(value) => { self.call_frames.pop(); @@ -589,11 +585,12 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![], - locals: vec![], - body: vec![StmtInstruction::Expr(ExprInstruction::Number( + parameters: [].into(), + locals: [].into(), + body: [StmtInstruction::Expr(ExprInstruction::Number( "5".to_string(), - ))], + ))] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Call { @@ -667,9 +664,7 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![], - locals: vec![], - body: vec![], + ..Default::default() }, }, StmtInstruction::Expr(ExprInstruction::Call { @@ -696,11 +691,12 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![], - locals: vec![], - body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Number( + parameters: [].into(), + locals: [].into(), + body: [StmtInstruction::ReturnExpr(ExprInstruction::Number( "5".to_string(), - ))], + ))] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Call { @@ -805,9 +801,9 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![], - locals: vec![], - body: vec![StmtInstruction::Quit], + parameters: [].into(), + locals: [].into(), + body: [StmtInstruction::Quit].into(), }, }, StmtInstruction::Expr(ExprInstruction::Number("1".to_string())), @@ -886,12 +882,13 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![], - locals: vec![Variable::Number('a')], - body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + parameters: [].into(), + locals: [Variable::Number('a')].into(), + body: [StmtInstruction::Expr(ExprInstruction::Assignment { named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Number("5".to_string())), - })], + })] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Call { @@ -921,12 +918,13 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![Variable::Number('a')], - locals: vec![], - body: vec![StmtInstruction::Expr(ExprInstruction::Assignment { + parameters: [Variable::Number('a')].into(), + locals: [].into(), + body: [StmtInstruction::Expr(ExprInstruction::Assignment { named: NamedExpr::VariableNumber('a'), value: Box::new(ExprInstruction::Number("5".to_string())), - })], + })] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Assignment { @@ -960,11 +958,12 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![Variable::Number('a')], - locals: vec![], - body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Named( + parameters: [Variable::Number('a')].into(), + locals: [].into(), + body: [StmtInstruction::ReturnExpr(ExprInstruction::Named( NamedExpr::VariableNumber('a'), - ))], + ))] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Call { @@ -996,9 +995,9 @@ mod tests { name: 'f', function: Function { name: 'f', - parameters: vec![Variable::Array('a')], - locals: vec![], - body: vec![ + parameters: [Variable::Array('a')].into(), + locals: [].into(), + body: [ StmtInstruction::Expr(ExprInstruction::Named(NamedExpr::ArrayItem { name: 'a', index: Box::new(ExprInstruction::Number("0".to_string())), @@ -1010,7 +1009,8 @@ mod tests { }, value: Box::new(ExprInstruction::Number("5".to_string())), }), - ], + ] + .into(), }, }, StmtInstruction::Expr(ExprInstruction::Assignment { diff --git a/calc/src/bc_util/parser.rs b/calc/src/bc_util/parser.rs index 98b2179eb..a992a5f2e 100644 --- a/calc/src/bc_util/parser.rs +++ b/calc/src/bc_util/parser.rs @@ -367,9 +367,9 @@ fn parse_function(func: Pair) -> Result } Ok(Function { name, - parameters, - locals, - body, + parameters: parameters.into(), + locals: locals.into(), + body: body.into(), }) } @@ -898,7 +898,7 @@ mod test { name: 'f', function: Function { name: 'f', - body: vec![StmtInstruction::Return], + body: [StmtInstruction::Return].into(), ..Default::default() } } @@ -914,9 +914,10 @@ mod test { name: 'f', function: Function { name: 'f', - body: vec![StmtInstruction::ReturnExpr(ExprInstruction::Number( + body: [StmtInstruction::ReturnExpr(ExprInstruction::Number( "1".to_string() - ))], + ))] + .into(), ..Default::default() } } @@ -1091,12 +1092,13 @@ mod test { func, Function { name: 'f', - parameters: vec![ + parameters: [ Variable::Array('a'), Variable::Number('b'), Variable::Number('c'), Variable::Array('d') - ], + ] + .into(), ..Default::default() } ); @@ -1109,12 +1111,13 @@ mod test { func, Function { name: 'f', - locals: vec![ + locals: [ Variable::Array('a'), Variable::Number('b'), Variable::Number('c'), Variable::Array('d') - ], + ] + .into(), ..Default::default() } ); @@ -1127,10 +1130,11 @@ mod test { func, Function { name: 'f', - body: vec![StmtInstruction::Expr(ExprInstruction::Add( + body: [StmtInstruction::Expr(ExprInstruction::Add( Box::new(ExprInstruction::Number("1".to_string())), Box::new(ExprInstruction::Number("2".to_string())) - ))], + ))] + .into(), ..Default::default() } ); From d3aa3a6e4ec358a63aabbf778012e401c9f20da3 Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 21:45:00 +0200 Subject: [PATCH 05/15] bc: calling an undefined function is a runtime error --- calc/src/bc_util/interpreter.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/calc/src/bc_util/interpreter.rs b/calc/src/bc_util/interpreter.rs index f9e19c1da..f5f23c03f 100644 --- a/calc/src/bc_util/interpreter.rs +++ b/calc/src/bc_util/interpreter.rs @@ -135,6 +135,9 @@ impl Interpreter { fn call_function(&mut self, name: char, args: &[FunctionArgument]) -> ExecutionResult { let function = &self.functions[name_index(name)].clone(); + if function.name == '\0' { + return Err("undefined function"); + } let mut call_frame = CallFrame::default(); for (arg, param) in args.iter().zip(function.parameters.iter()) { @@ -1051,4 +1054,17 @@ mod tests { .unwrap(); assert_eq!(output.string, "10\n"); } + + #[test] + fn test_call_undefined_function_is_error() { + let mut interpreter = Interpreter::default(); + // ``` + // f() + // ``` + let output = interpreter.exec(vec![StmtInstruction::Expr(ExprInstruction::Call { + name: 'f', + args: vec![], + })]); + assert!(output.is_err()); + } } From 7ecb41cfece63d6e369307879b47ecf9043d7c3e Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 21:45:17 +0200 Subject: [PATCH 06/15] bc: improve runtime error messages --- calc/src/bc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/calc/src/bc.rs b/calc/src/bc.rs index 00cb3bee4..82cebfbe2 100644 --- a/calc/src/bc.rs +++ b/calc/src/bc.rs @@ -39,7 +39,7 @@ fn exec_str(s: &str, interpreter: &mut Interpreter) -> bool { } } Err(e) => { - println!("{}", e); + println!("runtime error: {}", e); } }, Err(e) => { From 523fdeea2430a30bdfba38d3270cb012f672a812 Mon Sep 17 00:00:00 2001 From: grisenti Date: Mon, 27 May 2024 22:13:02 +0200 Subject: [PATCH 07/15] bc: set text domain --- calc/src/bc.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/calc/src/bc.rs b/calc/src/bc.rs index 82cebfbe2..434c760bc 100644 --- a/calc/src/bc.rs +++ b/calc/src/bc.rs @@ -15,6 +15,8 @@ use bc_util::{ }; use clap::Parser; +use gettextrs::{bind_textdomain_codeset, textdomain}; +use plib::PROJECT_NAME; use rustyline::{error::ReadlineError, DefaultEditor, Result}; mod bc_util; @@ -50,6 +52,9 @@ fn exec_str(s: &str, interpreter: &mut Interpreter) -> bool { } fn main() -> Result<()> { + textdomain(PROJECT_NAME)?; + bind_textdomain_codeset(PROJECT_NAME, "UTF-8")?; + let args = Args::parse(); let mut interpreter = Interpreter::default(); for file in args.files { From ee3fe1e1164910428b4724fc1444173e6ba4bc75 Mon Sep 17 00:00:00 2001 From: grisenti Date: Tue, 28 May 2024 09:21:03 +0200 Subject: [PATCH 08/15] bc: explain algorithm --- calc/src/bc_util/number.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs index 0148ca030..fe418d34f 100644 --- a/calc/src/bc_util/number.rs +++ b/calc/src/bc_util/number.rs @@ -142,7 +142,10 @@ impl Number { result.push('.'); let mut temp = BigDecimal::one(); - // TODO: what does this do? + // The standard doesn't specify how many fractional digits to print. + // Here, we set the scale of the number to the value smallest value of + // i such that: (base ^ i).digits() > scale. + // This method is also used in other implementations, including GNU bc. while temp.digits() <= scale { fractional_part *= base; let integer_part = fractional_part.with_scale(0); From 72d5dfe565d400efb6c5549f5de34a0244e0d712 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 09:26:42 +0200 Subject: [PATCH 09/15] bc: fix number to_string The previous implementation used scientific notation for very large and very small numbers and didn't print trailing zero if the fractional part was zero --- calc/src/bc_util/number.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs index fe418d34f..cc97d5738 100644 --- a/calc/src/bc_util/number.rs +++ b/calc/src/bc_util/number.rs @@ -99,9 +99,6 @@ impl Number { if self.is_zero() { return "0".to_string(); } - if base == 10 { - return self.0.to_string(); - } let scale = self.scale(); let base_ilog10 = base.ilog10(); let mut integer_part = self.0.with_scale(0); @@ -136,7 +133,7 @@ impl Number { } } - if fractional_part.is_zero() { + if fractional_part.fractional_digit_count() == 0 { return result; } @@ -309,6 +306,18 @@ mod tests { "123.456" ); assert_eq!(Number::parse(".1234", 10).unwrap().to_string(10), "0.1234"); + assert_eq!( + Number::parse("0.000000000000000000000000001", 10) + .unwrap() + .to_string(10), + "0.000000000000000000000000001" + ); + assert_eq!( + Number::parse("100000000000000000000000000000000000000000000000", 10) + .unwrap() + .to_string(10), + "100000000000000000000000000000000000000000000000", + ); } #[test] From 5021ad2c80a69951098087192010f39724727830 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 15:48:01 +0200 Subject: [PATCH 10/15] bc: fix number to_string printing negative numbers less than -1 panicked --- calc/src/bc_util/number.rs | 40 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs index cc97d5738..a519f57a1 100644 --- a/calc/src/bc_util/number.rs +++ b/calc/src/bc_util/number.rs @@ -1,7 +1,4 @@ -use bigdecimal::{ - num_bigint::{BigInt, Sign}, - BigDecimal, Num, One, Signed, ToPrimitive, Zero, -}; +use bigdecimal::{num_bigint::BigInt, BigDecimal, Num, One, Signed, ToPrimitive, Zero}; fn to_digit(c: u8) -> u8 { match c { @@ -95,22 +92,23 @@ impl Number { } /// Convert the number to a string in the given base. - pub fn to_string(self, base: u64) -> String { + pub fn to_string(mut self, base: u64) -> String { if self.is_zero() { return "0".to_string(); } + let scale = self.scale(); - let base_ilog10 = base.ilog10(); - let mut integer_part = self.0.with_scale(0); - let mut fractional_part = self.0 - &integer_part; let mut result = String::new(); - if integer_part.sign() == Sign::Minus { - integer_part = -integer_part; - fractional_part = -fractional_part; + if self.0.is_negative() { result.push('-'); + self.0 = -self.0; } + let base_ilog10 = base.ilog10(); + let integer_part = self.0.with_scale(0); + let mut fractional_part = self.0 - &integer_part; + if integer_part.is_zero() { result.push('0'); } else { @@ -300,12 +298,25 @@ mod tests { #[test] fn test_output_base_10() { assert_eq!(Number::from(1).to_string(10), "1"); - assert_eq!(Number::parse("123", 10).unwrap().to_string(10), "123"); + assert_eq!(Number::from(123).to_string(10), "123"); + assert_eq!(Number::from(123).negate().to_string(10), "-123"); assert_eq!( Number::parse("123.456", 10).unwrap().to_string(10), "123.456" ); + assert_eq!( + Number::parse("123.456", 10).unwrap().negate().to_string(10), + "-123.456" + ); assert_eq!(Number::parse(".1234", 10).unwrap().to_string(10), "0.1234"); + assert_eq!( + Number::parse(".1234", 10).unwrap().negate().to_string(10), + "-0.1234" + ); + assert_eq!( + Number::parse("2.000000", 10).unwrap().to_string(10), + "2.000000" + ); assert_eq!( Number::parse("0.000000000000000000000000001", 10) .unwrap() @@ -324,10 +335,15 @@ mod tests { fn test_output_base_2() { assert_eq!(Number::from(1).to_string(2), "1"); assert_eq!(Number::from(13).to_string(2), "1101"); + assert_eq!(Number::from(13).negate().to_string(2), "-1101"); assert_eq!( Number::parse("13.625", 10).unwrap().to_string(2), "1101.1010000000" ); + assert_eq!( + Number::parse("13.625", 10).unwrap().negate().to_string(2), + "-1101.1010000000" + ); assert_eq!( Number::parse("0.8125", 10).unwrap().to_string(2), "0.11010000000000" From afa278469ed18f75fc2c6bb3f31f6159d4caa086 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 18:37:07 +0200 Subject: [PATCH 11/15] bc: implement math library --- calc/src/bc.rs | 9 + calc/src/bc_util/math_functions.bc | 288 +++++++++++++++++++++++++++++ calc/tests/bc/atan_to_scale_17.bc | 14 ++ calc/tests/bc/atan_to_scale_17.out | 6 + calc/tests/bc/cos_to_scale_18.bc | 15 ++ calc/tests/bc/cos_to_scale_18.out | 6 + calc/tests/bc/ln_to_scale_17.bc | 15 ++ calc/tests/bc/ln_to_scale_17.out | 6 + calc/tests/bc/sin_to_scale_18.bc | 15 ++ calc/tests/bc/sin_to_scale_18.out | 6 + calc/tests/integration.rs | 45 +++++ 11 files changed, 425 insertions(+) create mode 100644 calc/src/bc_util/math_functions.bc create mode 100644 calc/tests/bc/atan_to_scale_17.bc create mode 100644 calc/tests/bc/atan_to_scale_17.out create mode 100644 calc/tests/bc/cos_to_scale_18.bc create mode 100644 calc/tests/bc/cos_to_scale_18.out create mode 100644 calc/tests/bc/ln_to_scale_17.bc create mode 100644 calc/tests/bc/ln_to_scale_17.out create mode 100644 calc/tests/bc/sin_to_scale_18.bc create mode 100644 calc/tests/bc/sin_to_scale_18.out diff --git a/calc/src/bc.rs b/calc/src/bc.rs index 434c760bc..194153ae3 100644 --- a/calc/src/bc.rs +++ b/calc/src/bc.rs @@ -57,6 +57,15 @@ fn main() -> Result<()> { let args = Args::parse(); let mut interpreter = Interpreter::default(); + + if args.define_math_functions { + let lib = parse_program(include_str!("bc_util/math_functions.bc")) + .expect("error parsing standard math functions"); + interpreter + .exec(lib) + .expect("error loading standard math functions"); + } + for file in args.files { match std::fs::read_to_string(&file) { Ok(s) => { diff --git a/calc/src/bc_util/math_functions.bc b/calc/src/bc_util/math_functions.bc new file mode 100644 index 000000000..59ae9bd8b --- /dev/null +++ b/calc/src/bc_util/math_functions.bc @@ -0,0 +1,288 @@ +/* +* Copyright (c) 2024 Hemi Labs, Inc. +* +* This file is part of the posixutils-rs project covered under +* the MIT License. For the full license text, please see the LICENSE +* file in the root directory of this project. +* SPDX-License-Identifier: MIT +*/ + + +/* +Uses sin(x) = x - x^3/3! + x^5/5! - x^7/7! + ... +(http://www.hvks.com/Numerical/Downloads/HVE%20Fast%20Trigonometric%20functions%20for%20arbitrary%20precision.pdf) +*/ +define s(x) { + auto b, y, f, i, v, r, z, p, s, m; + + /* b: previous ibase */ + /* y: x^i */ + /* f: i! */ + /* i: index of the current Taylor term */ + /* v: y / f */ + /* r: current estimate for sin(x) */ + /* z: -x^2 */ + /* p: the value of pi */ + /* s: previous scale */ + /* m: 1 if pi has been subtracted from x */ + + b = ibase; + ibase = A; + + p = 4 * a(1); + + /* sin(x) = sin(x mod (2 * pi)) */ + s = scale; + scale = 0; + x %= 2 * p; + scale = s; + + /* sin(x) = -sin(x - pi) for x >= pi */ + if (x >= p) { + x -= p; + m = 1; + } + + r = y = x; + z = -(x * x); + v = 1; + f = 1; + for (i = 3; v; i += 2) { + y *= z; + f *= (i - 1) * i; + v = y / f; + r += v; + } + + ibase = b; + + if (m) return(-r); + return(r); +} + +/* +Uses cos(x) = sin(x + pi/2) +*/ +define c(x) { + auto b, r; + b = ibase; + ibase = A; + r = s(x + 2 * a(1)) + ibase = b; + return(r); +} + +/* +Uses atan(x) = x - x^3/3 + x^5/5 - x^7/7 + ... +The function uses the identity atan(x) = 2 * atan(y / (1 + sqrt(1 + y^2))) +to get the value of |x| below 1. +(http://www.hvks.com/Numerical/Downloads/HVE%20Fast%20Trigonometric%20functions%20for%20arbitrary%20precision.pdf) +*/ +define a(x) { + auto b, r, i, y, d, m, z, v; + + /* b: previous ibase */ + /* r: current estimate for atan(x) */ + /* i: index of the current Taylor term */ + /* y: x^i */ + /* d: 2 if x has been scaled, 1 otherwise */ + /* m: 1 if x is negative */ + /* z: -x^2 */ + /* v: y / i */ + + + b = ibase; + ibase = A; + + if (x < 0) { + m = 1; + x = -x; + } + + d = 1; + if (x >= 1) { + x = x / (1 + sqrt(1 + x * x)); + d = 2; + } + + y = r = x; + z = -(x * x); + v = 1; + for (i = 3; v; i += 2) { + y *= z; + v = y / i; + r += v; + } + + ibase = b; + + r *= d; + if (m) return(-r); + return(r); +} + +/* +Uses ln(x) = 2 * artanh(y) = 2 * (y + y^3/3 + y^5/5 + ...) +where y = (x - 1) / (x + 1). +The function uses the identity l(x) = 2 * l(sqrt(x)) to get the value of x +between 0.9 and 1.2 (values chosen through testing). +(http://www.hvks.com/Numerical/Downloads/HVE%20Fast%20Log()%20calculation%20for%20arbitrary%20precision.pdf) +*/ +define l(x) { + auto b, r, i, d, v, y, z; + + /* b: previous ibase */ + /* r: current estimate for ln(x) */ + /* i: index of the current Taylor term */ + /* d: the exponent of the result */ + /* v: y / i */ + /* y: ((x - 1) / (x + 1))^i */ + /* z: y^2 */ + + b = ibase; + ibase = A; + + /* the function is not defined for x < 0 + this matches the behavior of GNU bc */ + if(x <= 0) return((1 - 10^scale) / 1) + + d = 1; + while(x > 1.2) { + x = sqrt(x); + d *= 2; + } + while (x < 0.9) { + x = sqrt(x); + d *= 2; + } + + y = (x - 1) / (x + 1); + z = y * y; + r = y; + v = 1; + for(i = 3; v; i += 2) { + y *= z; + v = y / i; + r += v; + } + + ibase = b; + + return (d * 2 * r) +} + +/* +Uses e ^ x = 1 + x + x/2! + x/3! + ... +For x > 1, the Taylor series converges very slowly, so the +function uses the identity e(x) = e(x/2)^2 to get the value of x +under 1. +If x is negative we negate x and divide compute the reciprocal of +the result at the end. +(http://www.hvks.com/Numerical/Downloads/HVE%20Fast%20Exp()%20calculation%20for%20arbitrary%20precision.pdf) +*/ +define e(x) { + auto f, y, v, i, r, d, s, b, m; + + /* f: i! */ + /* y: x^i */ + /* v: y / f */ + /* i: index of the current Taylor term */ + /* r: current estimate for e^x */ + /* d: the exponent of the result */ + /* s: previous scale */ + /* b: previous ibase */ + /* m: 1 if x is negative */ + + b = ibase; + s = scale; + ibase = A; + + if (x < 1) { + m = 1; + x = -x; + } + + /* each division by 2 can add at most one fractional digit to + x so we need to increase scale by one on every iteration */ + scale = scale(x) + 1; + d = 1; + while (x > 1) { + x /= 2; + d *= 2; + scale += 1; + } + + scale = s; + + f = 1; + v = 1; + y = x; + r = 1 + x; + /* the loop runs until y / f is not 0, meaning + until the number can be represented using the current scale */ + for (i = 2; v; ++i) { + f *= i; + y *= x; + v = y / f; + r += v; + } + r ^= d; + + ibase = b; + scale = s; + + if (m) return(1 / r); + return(r/1); +} + +/* +Uses j(n, x) = x^n / (2^n * n!) * (1 - x^2/(2^2*1!*(n+1)) + x^4/(2^4*2!*(n+1)*(n+2)) - x^6/(2^6*3!*(n+1)*(n+2)*(n+3)) ...) +and j(-n, x) = (-1)^n * j(n, x) +*/ +define j(n, x) { + auto b, r, y, v, z, i, d, g, m, s; + + /* b: previous ibase */ + /* r: current estimate for j(n, x) */ + /* y: x^(2i) */ + /* v: y / d */ + /* z: -x^2 */ + /* i: index of the current Taylor term */ + /* d: the denominator of the above series */ + /* g: x^n / (2^n * n!) */ + /* m: 1 if n is negative */ + /* s: previous scale */ + + b = ibase; + + s = scale; + scale = 0; + /* make n an integer */ + n /= 1; + if (n < 0) { + n = -n; + if (n % 2) m = 1; + } + scale = s; + + y = r = v = 1; + z = -(x * x); + d = 1; + for (i = 1; v; ++i) { + y *= z; + d *= 4 * i * (n + i); + v = y / d; + r += v; + } + + g = 1; + for (i = 1; i <= n; ++i) g *= i; + g = x^n / (2^n * g); + + ibase = b; + + if (m) return(-g * r); + return(g * r); +} + +scale = 20; diff --git a/calc/tests/bc/atan_to_scale_17.bc b/calc/tests/bc/atan_to_scale_17.bc new file mode 100644 index 000000000..bec0c85fa --- /dev/null +++ b/calc/tests/bc/atan_to_scale_17.bc @@ -0,0 +1,14 @@ +a = a(0.2) +b = a(0.6) +c = a(1) +d = a(1.4) +e = a(1.8) + +scale = 17 +a/1 +b/1 +c/1 +d/1 +d/1 +e/1 +quit diff --git a/calc/tests/bc/atan_to_scale_17.out b/calc/tests/bc/atan_to_scale_17.out new file mode 100644 index 000000000..1161b4685 --- /dev/null +++ b/calc/tests/bc/atan_to_scale_17.out @@ -0,0 +1,6 @@ +0.19739555984988075 +0.54041950027058415 +0.78539816339744830 +0.95054684081207514 +0.95054684081207514 +1.06369782240255966 diff --git a/calc/tests/bc/cos_to_scale_18.bc b/calc/tests/bc/cos_to_scale_18.bc new file mode 100644 index 000000000..7754f553a --- /dev/null +++ b/calc/tests/bc/cos_to_scale_18.bc @@ -0,0 +1,15 @@ +a = c(0.5) +b = c(1) +c = c(1.5) +d = c(2) +e = c(2.5) +f = c(3) + +scale = 18 +a/1 +b/1 +c/1 +d/1 +e/1 +f/1 +quit diff --git a/calc/tests/bc/cos_to_scale_18.out b/calc/tests/bc/cos_to_scale_18.out new file mode 100644 index 000000000..89d824051 --- /dev/null +++ b/calc/tests/bc/cos_to_scale_18.out @@ -0,0 +1,6 @@ +0.877582561890372716 +0.540302305868139717 +0.070737201667702910 +-0.416146836547142386 +-0.801143615546933714 +-0.989992496600445457 diff --git a/calc/tests/bc/ln_to_scale_17.bc b/calc/tests/bc/ln_to_scale_17.bc new file mode 100644 index 000000000..a7d0ea958 --- /dev/null +++ b/calc/tests/bc/ln_to_scale_17.bc @@ -0,0 +1,15 @@ +a = l(0.2) +b = l(0.6) +c = l(2) +d = l(14.9867) +e = l(130.230890) +f = l(1302.9012834) + +scale = 17 +a/1 +b/1 +c/1 +d/1 +e/1 +f/1 +quit diff --git a/calc/tests/bc/ln_to_scale_17.out b/calc/tests/bc/ln_to_scale_17.out new file mode 100644 index 000000000..0f2ec0d33 --- /dev/null +++ b/calc/tests/bc/ln_to_scale_17.out @@ -0,0 +1,6 @@ +-1.60943791243410037 +-0.51082562376599068 +0.69314718055994530 +2.70716314111414067 +4.86930895201907172 +7.17234881324178779 diff --git a/calc/tests/bc/sin_to_scale_18.bc b/calc/tests/bc/sin_to_scale_18.bc new file mode 100644 index 000000000..743414dc3 --- /dev/null +++ b/calc/tests/bc/sin_to_scale_18.bc @@ -0,0 +1,15 @@ +a = s(0.5) +b = s(1) +c = s(1.5) +d = s(2) +e = s(2.5) +f = s(3) + +scale = 18 +a/1 +b/1 +c/1 +d/1 +e/1 +f/1 +quit diff --git a/calc/tests/bc/sin_to_scale_18.out b/calc/tests/bc/sin_to_scale_18.out new file mode 100644 index 000000000..e57486500 --- /dev/null +++ b/calc/tests/bc/sin_to_scale_18.out @@ -0,0 +1,6 @@ +0.479425538604203000 +0.841470984807896506 +0.997494986604054430 +0.909297426825681695 +0.598472144103956494 +0.141120008059867222 diff --git a/calc/tests/integration.rs b/calc/tests/integration.rs index 1088345f8..1278a7431 100644 --- a/calc/tests/integration.rs +++ b/calc/tests/integration.rs @@ -33,6 +33,17 @@ fn test_bc(program: &str, expected_output: &str) { }); } +fn test_bc_with_math_library(program: &str, expected_output: &str) { + run_test(TestPlan { + cmd: String::from("bc"), + args: vec!["-l".to_string()], + stdin_data: program.to_string(), + expected_out: String::from(expected_output), + expected_err: String::from(""), + expected_exit_code: 0, + }); +} + macro_rules! test_bc { ($test_name:ident) => { test_bc( @@ -42,6 +53,15 @@ macro_rules! test_bc { }; } +macro_rules! test_bc_l { + ($test_name:ident) => { + test_bc_with_math_library( + include_str!(concat!("bc/", stringify!($test_name), ".bc")), + include_str!(concat!("bc/", stringify!($test_name), ".out")), + ) + }; +} + #[test] fn test_expr_logops() { expr_test(&["4", "|", "5", "+", "1"], "5\n"); @@ -299,3 +319,28 @@ fn test_bc_uninitialized_variables_are_zero() { fn test_bc_while_loop() { test_bc!(while_loop) } + +#[test] +fn test_bc_compile_math_library() { + test_bc_with_math_library("quit\n", ""); +} + +#[test] +fn test_bc_ln_to_scale_17() { + test_bc_l!(ln_to_scale_17) +} + +#[test] +fn test_bc_atan_to_scale_17() { + test_bc_l!(atan_to_scale_17) +} + +#[test] +fn test_bc_sin_to_scale_18() { + test_bc_l!(sin_to_scale_18) +} + +#[test] +fn test_bc_cos_to_scale_18() { + test_bc_l!(cos_to_scale_18) +} From 38230a600a01a4b106f4bd2850fed0d580b2417a Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 18:37:57 +0200 Subject: [PATCH 12/15] bc: fix unused function warning --- calc/src/bc_util/number.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs index a519f57a1..ae9020fd4 100644 --- a/calc/src/bc_util/number.rs +++ b/calc/src/bc_util/number.rs @@ -35,10 +35,6 @@ impl Number { Self(BigDecimal::zero()) } - pub fn one() -> Self { - Self(BigDecimal::one()) - } - pub fn as_u64(&self) -> Option { self.0.to_u64() } From 720df0d090d948e7dca0601e848fe90242a12206 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 18:47:05 +0200 Subject: [PATCH 13/15] bc: remove unused type --- calc/src/bc_util/parser.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/calc/src/bc_util/parser.rs b/calc/src/bc_util/parser.rs index a992a5f2e..fad65e5d2 100644 --- a/calc/src/bc_util/parser.rs +++ b/calc/src/bc_util/parser.rs @@ -379,16 +379,6 @@ pub struct BcParser; pub type Program = Vec; -pub struct ParseError { - err: pest::error::Error, -} - -impl std::fmt::Display for ParseError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.err) - } -} - pub fn parse_program(text: &str) -> Result> { let program = BcParser::parse(Rule::program, text)?.next().unwrap(); let mut result = Vec::new(); From bc8a2f7321aa5dc01c27eb9bbcbdc7bd99dd6ee7 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 18:58:31 +0200 Subject: [PATCH 14/15] bc: improve comments in parser --- calc/src/bc_util/parser.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/calc/src/bc_util/parser.rs b/calc/src/bc_util/parser.rs index fad65e5d2..b06e2c7a3 100644 --- a/calc/src/bc_util/parser.rs +++ b/calc/src/bc_util/parser.rs @@ -132,6 +132,7 @@ fn parse_primary(expr: Pair) -> ExprInstruction { Rule::number => ExprInstruction::Number(to_bc_number(expr.as_str())), Rule::paren => parse_expr(first_child(expr)), Rule::builtin_call => { + // name ( expr ) let mut inner = expr.into_inner(); let func = match inner.next().unwrap().as_str() { "length" => BuiltinFunction::Length, @@ -275,6 +276,7 @@ fn parse_stmt( statements.push(StmtInstruction::Quit); } Rule::return_stmt => { + // return ( "(" expr? ")" )? if !in_function { return Err(pest::error::Error::new_from_span( pest::error::ErrorVariant::CustomError { @@ -291,6 +293,7 @@ fn parse_stmt( } } Rule::if_stmt => { + // if (condition) stmt let mut inner = stmt.into_inner(); let condition = parse_condition(inner.next().unwrap()); let mut body = Vec::new(); @@ -339,6 +342,9 @@ fn parse_stmt( fn parse_function(func: Pair) -> Result> { let mut function = func.into_inner(); + + // define letter ( parameter_list ) auto_define_list statement_list end + let name = as_letter(function.next().unwrap()); let mut parameters = Vec::new(); @@ -385,6 +391,7 @@ pub fn parse_program(text: &str) -> Result> { for item in program.into_inner() { match item.as_rule() { Rule::semicolon_list => { + // stmt* for stmt in item.into_inner() { parse_stmt(stmt, false, false, &mut result)?; } @@ -410,11 +417,23 @@ fn location_end(loc: InputLocation) -> usize { } } +/// Returns `true` if `text` contains an incomplete expression or statement. +/// # Examples +/// ``` +/// assert!(is_incomplete("1 + 2 *\\\n")) +/// assert!(is_incomplete("define f() {\n")) +/// assert!(is_incomplete("if (c) {\n")) +/// assert!(is_incomplete("while (c) {\n")) +/// ``` pub fn is_incomplete(text: &str) -> bool { match parse_program(text) { Ok(_) => false, Err(e) => { let pos = location_end(e.location); + // The program is incomplete if either: + // - we expect something after the end of the input + // - the error occurs at the start of and incomplete comment + // - the error occurs at the start of an incomplete string pos == text.len() || text.as_bytes()[pos..text.len().min(pos + 2)] == [b'/', b'*'] || text.as_bytes()[pos] == b'"' From 89d222bc7bcbb38e4d9544b8a83955fba30ea7e2 Mon Sep 17 00:00:00 2001 From: grisenti Date: Thu, 30 May 2024 19:10:59 +0200 Subject: [PATCH 15/15] bc: improve comments --- calc/src/bc_util/number.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/calc/src/bc_util/number.rs b/calc/src/bc_util/number.rs index ae9020fd4..7c8f13fd2 100644 --- a/calc/src/bc_util/number.rs +++ b/calc/src/bc_util/number.rs @@ -1,5 +1,8 @@ use bigdecimal::{num_bigint::BigInt, BigDecimal, Num, One, Signed, ToPrimitive, Zero}; +/// Converts a character to a number +/// # Panics +/// panics if the character is not a valid hexadecimal digit fn to_digit(c: u8) -> u8 { match c { b'0'..=b'9' => c - b'0', @@ -8,6 +11,9 @@ fn to_digit(c: u8) -> u8 { } } +/// Convert a number to a character +/// # Panics +/// panics if the number is bigger than 15 fn to_char(val: u8) -> char { match val { 0..=9 => (val + b'0') as char, @@ -16,6 +22,7 @@ fn to_char(val: u8) -> char { } } +/// converts a number to a string of len `base_ilog10`, padding with zeros fn pad_digit(d: u64, base_ilog10: u32) -> String { let width = base_ilog10 + 1; format!("{:0width$}", d, width = width as usize) @@ -43,7 +50,12 @@ impl Number { /// # Returns /// `None` if the string contains invalid characters for the given base. /// # Panics - /// panics if the string is empty or if base is not in the range 2..=16. + /// panics if: + /// - the string is empty + /// - `base` is not in the range 2..=16. + /// - `s` does not contain a valid number + /// + /// all the above should have been already checked by the parser pub fn parse(s: &str, base: u64) -> Option { assert!(!s.is_empty(), "parsed number has no digits"); assert!((2..=16).contains(&base), "base must be in the range 2..=16");