diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..bceddd5 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,26 @@ +name: Rust + +on: + push: + branches: [ master, ci-execution ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +defaults: + run: + working-directory: ./compiler-rs + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build + run: cargo build --verbose + - name: Ensure formatted + run: cargo fmt --all -- --check + - name: Run Cargo tests + run: cargo test \ No newline at end of file diff --git a/compiler-rs/.gitignore b/compiler-rs/.gitignore new file mode 100644 index 0000000..0eec03d --- /dev/null +++ b/compiler-rs/.gitignore @@ -0,0 +1,11 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/compiler-rs/Cargo.toml b/compiler-rs/Cargo.toml new file mode 100644 index 0000000..01cda2f --- /dev/null +++ b/compiler-rs/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "eel_wasm" +version = "0.1.0" +authors = ["Jordan Eldredge "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +default = ["console_error_panic_hook"] + +[dependencies] +wasm-bindgen = "0.2.63" +parity-wasm = "0.42.2" +structopt = "0.3.21" +indexmap = "1.6.2" + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + + +[dev-dependencies] +# https://github.com/paritytech/wasmi/issues/252 +# wasmi = { path = "../../wasmi"} +wasmi = { git = "https://github.com/paritytech/wasmi", branch = "master" } +wabt = "0.9.0" +wasm-bindgen-test = "0.3.13" +wasmprinter = "0.2.0" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" + diff --git a/compiler-rs/README.md b/compiler-rs/README.md new file mode 100644 index 0000000..0740c46 --- /dev/null +++ b/compiler-rs/README.md @@ -0,0 +1,19 @@ +## Build + +To build the Wasm module: +```bash +wasm-pack build +``` + +You can find the output in `pkg/`. + +To check the resulting wasm size +```bash +gzip -9 < pkg/optimized.wasm | wc -c +``` + +## TODO + +- [ ] Add AST node for arguments list so that we can show it as the error node when arg count is wrong. +- [ ] Run `wasm-pack build` in CI. +- [ ] Should the magicness of reg10 values be case insensitive? (It is in the JS version) \ No newline at end of file diff --git a/compiler-rs/src/ast.rs b/compiler-rs/src/ast.rs new file mode 100644 index 0000000..32c7079 --- /dev/null +++ b/compiler-rs/src/ast.rs @@ -0,0 +1,102 @@ +use crate::span::Span; + +#[derive(Debug, PartialEq)] +pub struct EelFunction { + pub expressions: ExpressionBlock, +} + +#[derive(Debug, PartialEq)] +pub struct ExpressionBlock { + pub expressions: Vec, +} + +#[derive(Debug, PartialEq)] +pub enum Expression { + BinaryExpression(BinaryExpression), + UnaryExpression(UnaryExpression), + NumberLiteral(NumberLiteral), + Assignment(Assignment), + FunctionCall(FunctionCall), + ExpressionBlock(ExpressionBlock), + Identifier(Identifier), +} + +#[derive(Debug, PartialEq)] +pub struct UnaryExpression { + pub right: Box, + pub op: UnaryOperator, +} + +#[derive(Debug, PartialEq)] +pub struct BinaryExpression { + pub left: Box, + pub right: Box, + pub op: BinaryOperator, +} + +#[derive(Debug, PartialEq)] +pub struct NumberLiteral { + pub value: f64, +} + +#[derive(Debug, PartialEq)] +pub enum BinaryOperator { + Add, + Subtract, + Multiply, + Divide, + Mod, + Eq, + BitwiseAnd, + BitwiseOr, + LogicalAnd, + LogicalOr, + Pow, + LessThan, + GreaterThan, + LessThanEqual, + GreaterThanEqual, + NotEqual, +} + +#[derive(Debug, PartialEq)] +pub enum UnaryOperator { + Plus, + Minus, + Not, +} + +#[derive(Debug, PartialEq)] +pub struct Identifier { + pub name: String, + pub span: Span, +} + +#[derive(Debug, PartialEq)] +pub enum AssignmentOperator { + Equal, + PlusEqual, + MinusEqual, + TimesEqual, + DivEqual, + ModEqual, +} + +#[derive(Debug, PartialEq)] +pub struct Assignment { + pub left: AssignmentTarget, + pub operator: AssignmentOperator, + pub right: Box, +} + +#[derive(Debug, PartialEq)] +pub enum AssignmentTarget { + Identifier(Identifier), + FunctionCall(FunctionCall), +} + +#[derive(Debug, PartialEq)] +pub struct FunctionCall { + pub name: Identifier, + pub arguments: Vec, +} diff --git a/compiler-rs/src/bin/compiler.rs b/compiler-rs/src/bin/compiler.rs new file mode 100644 index 0000000..bd26734 --- /dev/null +++ b/compiler-rs/src/bin/compiler.rs @@ -0,0 +1,52 @@ +use eel_wasm::compile; +use std::io::{self, Write}; +use std::process; +use std::{collections::HashMap, fs}; + +use std::path::PathBuf; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(name = "eel-wasm", about = "Compile Eel code to WebAssembly.")] +struct Opt { + /// Input file + #[structopt(parse(from_os_str))] + input: PathBuf, + + /// Output file, stdout if not present + #[structopt(parse(from_os_str))] + output: Option, +} + +fn main() { + let opt = Opt::from_args(); + let filename = opt.input; + let source = fs::read_to_string(filename).unwrap_or_else(|err| { + eprintln!("Error reading file: {}", err); + process::exit(1); + }); + + let result = compile( + vec![("test".to_string(), &source, "pool".to_string())], + HashMap::default(), + ) + .unwrap_or_else(|err| { + eprintln!("{:?}", err); + process::exit(1); + }); + + match opt.output { + Some(output) => { + fs::write(output, result).unwrap_or_else(|err| { + eprintln!("Error writing output: {}", err); + process::exit(1); + }); + } + None => { + io::stdout().write_all(&result).unwrap_or_else(|err| { + eprintln!("Error writing to stdout: {}", err); + process::exit(1); + }); + } + } +} diff --git a/compiler-rs/src/builtin_functions.rs b/compiler-rs/src/builtin_functions.rs new file mode 100644 index 0000000..c5919f1 --- /dev/null +++ b/compiler-rs/src/builtin_functions.rs @@ -0,0 +1,227 @@ +use parity_wasm::elements::{ + BlockType, FuncBody, FunctionType, Instruction, Instructions, Local, ValueType, +}; + +use crate::utils::f64_const; +use crate::EelFunctionType; +use crate::{ + constants::{BUFFER_SIZE, EPSILON}, + emitter_context::EmitterContext, +}; + +#[derive(PartialEq, Eq, Hash)] +pub enum BuiltinFunction { + Div, + Mod, + GetBufferIndex, + LogicalOr, + LogicalAnd, + BitwiseAnd, + BitwiseOr, + Sqr, + Sign, +} + +impl BuiltinFunction { + pub fn get_type(&self) -> EelFunctionType { + FunctionType::new(vec![ValueType::F64; self.arity()], vec![self.return_type()]) + } + + pub fn return_type(&self) -> ValueType { + match self { + Self::GetBufferIndex => ValueType::I32, + _ => ValueType::F64, + } + } + + fn arity(&self) -> usize { + match self { + Self::Div => 2, + Self::GetBufferIndex => 1, + Self::Mod => 2, + Self::BitwiseAnd => 2, + Self::BitwiseOr => 2, + Self::LogicalAnd => 2, + Self::LogicalOr => 2, + Self::Sqr => 1, + Self::Sign => 1, + } + } + + pub fn func_body(&self, _context: &EmitterContext) -> FuncBody { + match self { + Self::Sign => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::F64Const(f64_const(0.0)), + Instruction::GetLocal(0), + Instruction::F64Lt, + Instruction::GetLocal(0), + Instruction::F64Const(f64_const(0.0)), + Instruction::F64Lt, + Instruction::I32Sub, + Instruction::F64ConvertSI32, + Instruction::End, + ]), + ), + Self::Div => FuncBody::new( + vec![Local::new(1, ValueType::I32)], + Instructions::new(vec![ + Instruction::GetLocal(1), + Instruction::F64Const(0), + Instruction::F64Ne, + Instruction::If(BlockType::Value(ValueType::F64)), + Instruction::GetLocal(0), + Instruction::GetLocal(1), + Instruction::F64Div, + Instruction::Else, + Instruction::F64Const(0), + Instruction::End, + Instruction::End, + ]), + ), + // Takes a float buffer index and converts it to an int. Values out of range + // are returned as `-1`. + // + // NOTE: There's actually a subtle bug that exists in Milkdrop's Eel + // implementation, which we reproduce here. + // + // Wasm's `trunc()` rounds towards zero. This means that for index `-1` we + // will return zero, since: `roundTowardZero(-1 + EPSILON) == 0` + // + // A subsequent check handles negative indexes, so negative indexes > than + // `-1` are not affected. + Self::GetBufferIndex => FuncBody::new( + vec![Local::new(1, ValueType::F64), Local::new(1, ValueType::I32)], + Instructions::new(vec![ + Instruction::F64Const(f64_const(EPSILON)), + Instruction::GetLocal(0), + Instruction::F64Add, + // STACK: [$i + EPSILON] + Instruction::TeeLocal(1), // $with_near + Instruction::I32TruncSF64, + // TODO We could probably make this a tee and get rid of the next get if we swap the final condition + Instruction::SetLocal(2), + // STACK: [] + Instruction::I32Const(-1), + Instruction::GetLocal(2), + // STACK: [-1, $truncated] + Instruction::I32Const(8), + Instruction::I32Mul, + // STACK: [-1, $truncated * 8] + Instruction::GetLocal(2), // $truncated + Instruction::I32Const(0), + // STACK: [-1, $truncated * 8, $truncated, 0] + Instruction::I32LtS, + // STACK: [-1, $truncated * 8, ] + Instruction::GetLocal(2), // $truncated + Instruction::I32Const(BUFFER_SIZE as i32 - 1), + Instruction::I32GtS, + // STACK: [-1, $truncated * 8, , ] + Instruction::I32Or, + // STACK: [-1, $truncated * 8, ] + Instruction::Select, + Instruction::End, + ]), + ), + Self::Mod => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(1), + Instruction::F64Const(f64_const(0.0)), + Instruction::F64Ne, + Instruction::If(BlockType::Value(ValueType::F64)), + Instruction::GetLocal(0), + Instruction::I64TruncSF64, + Instruction::GetLocal(1), + Instruction::I64TruncSF64, + Instruction::I64RemS, + Instruction::F64ConvertSI64, + Instruction::Else, + Instruction::F64Const(f64_const(0.0)), + Instruction::End, + Instruction::End, + ]), + ), + // TODO: This could probably be inlined + Self::BitwiseAnd => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(0), + Instruction::I64TruncSF64, + Instruction::GetLocal(1), + Instruction::I64TruncSF64, + Instruction::I64And, + Instruction::F64ConvertSI64, + Instruction::End, + ]), + ), + // TODO: This could probably be inlined + Self::BitwiseOr => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(0), + Instruction::I64TruncSF64, + Instruction::GetLocal(1), + Instruction::I64TruncSF64, + Instruction::I64Or, + Instruction::F64ConvertSI64, + Instruction::End, + ]), + ), + Self::Sqr => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(0), + Instruction::GetLocal(0), + Instruction::F64Mul, + Instruction::End, + ]), + ), + Self::LogicalAnd => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(0), + // is not zeroish + Instruction::F64Abs, + Instruction::F64Const(f64_const(EPSILON)), + Instruction::F64Gt, + // end is not zeroish + Instruction::GetLocal(1), + // is not zeroish + Instruction::F64Abs, + Instruction::F64Const(f64_const(EPSILON)), + Instruction::F64Gt, + // end is not zeroish + Instruction::I32And, + Instruction::I32Const(0), + Instruction::I32Ne, + Instruction::F64ConvertSI32, + Instruction::End, + ]), + ), + Self::LogicalOr => FuncBody::new( + vec![], + Instructions::new(vec![ + Instruction::GetLocal(0), + // is not zeroish + Instruction::F64Abs, + Instruction::F64Const(f64_const(EPSILON)), + Instruction::F64Gt, + // end is not zeroish + Instruction::GetLocal(1), + // is not zeroish + Instruction::F64Abs, + Instruction::F64Const(f64_const(EPSILON)), + Instruction::F64Gt, + // end is not zeroish + Instruction::I32Or, + Instruction::I32Const(0), + Instruction::I32Ne, + Instruction::F64ConvertSI32, + Instruction::End, + ]), + ), + } + } +} diff --git a/compiler-rs/src/constants.rs b/compiler-rs/src/constants.rs new file mode 100644 index 0000000..5e20758 --- /dev/null +++ b/compiler-rs/src/constants.rs @@ -0,0 +1,12 @@ +pub static EPSILON: f64 = 0.00001; + +pub static WASM_PAGE_SIZE: u32 = 65536; + +static BYTES_PER_F64: u32 = 8; +static BUFFER_COUNT: u32 = 2; + +// The number of items allowed in each buffer (megabuf/gmegabuf). +// https://github.com/WACUP/vis_milk2/blob/de9625a89e724afe23ed273b96b8e48496095b6c/ns-eel2/ns-eel.h#L145 +pub static BUFFER_SIZE: u32 = 65536 * 128; + +pub static WASM_MEMORY_SIZE: u32 = (BUFFER_SIZE * BYTES_PER_F64 * BUFFER_COUNT) / WASM_PAGE_SIZE; diff --git a/compiler-rs/src/emitter_context.rs b/compiler-rs/src/emitter_context.rs new file mode 100644 index 0000000..6ba0d93 --- /dev/null +++ b/compiler-rs/src/emitter_context.rs @@ -0,0 +1,56 @@ +use crate::{ + builtin_functions::BuiltinFunction, index_store::IndexStore, shim::Shim, EelFunctionType, +}; + +#[derive(PartialEq, Eq, Hash)] +pub enum ModuleFunction { + Shim(Shim), + Builtin(BuiltinFunction), + Eel(usize), +} + +pub struct EmitterContext { + pub current_pool: String, + pub globals: IndexStore<(Option, String)>, + pub functions: IndexStore, + pub function_types: IndexStore, +} + +impl EmitterContext { + pub fn new() -> Self { + Self { + current_pool: "".to_string(), // TODO: Is this okay to be empty? + globals: Default::default(), + function_types: Default::default(), + functions: IndexStore::new(), + } + } + pub fn resolve_variable(&mut self, name: String) -> u32 { + let pool = if variable_is_register(&name) { + None + } else { + Some(self.current_pool.clone()) + }; + + self.globals.get((pool, name)) + } + + pub fn resolve_shim_function(&mut self, shim: Shim) -> u32 { + self.functions.get(ModuleFunction::Shim(shim)) + } + + pub fn resolve_builtin_function(&mut self, builtin: BuiltinFunction) -> u32 { + self.function_types.ensure(builtin.get_type()); + self.functions.get(ModuleFunction::Builtin(builtin)) + } + + pub fn resolve_eel_function(&mut self, idx: usize) -> u32 { + self.functions.get(ModuleFunction::Eel(idx)) + } +} + +fn variable_is_register(name: &str) -> bool { + let chars: Vec<_> = name.chars().collect(); + // We avoided pulling in the regex crate! (But at what cost?) + matches!(chars.as_slice(), ['r', 'e', 'g', '0'..='9', '0'..='9']) +} diff --git a/compiler-rs/src/error.rs b/compiler-rs/src/error.rs new file mode 100644 index 0000000..b23429f --- /dev/null +++ b/compiler-rs/src/error.rs @@ -0,0 +1,23 @@ +use crate::span::Span; + +#[derive(Debug, PartialEq)] +pub struct CompilerError { + message: String, + span: Span, +} + +impl CompilerError { + pub fn new(message: String, span: Span) -> Self { + Self { + span, + message: message.into(), + } + } + + // TODO: Print a code frame + pub fn pretty_print(&self, _source: &str) -> String { + self.message.clone() + } +} + +pub type EmitterResult = Result; diff --git a/compiler-rs/src/file_chars.rs b/compiler-rs/src/file_chars.rs new file mode 100644 index 0000000..28bfe18 --- /dev/null +++ b/compiler-rs/src/file_chars.rs @@ -0,0 +1,37 @@ +use std::mem; +use std::str::Chars; + +pub const NULL: char = '\0'; + +pub struct FileChars<'a> { + chars: Chars<'a>, + pub next: char, + pub pos: u32, +} + +impl<'a> FileChars<'a> { + pub fn new(source: &'a str) -> Self { + let mut chars = source.chars(); + let next = chars.next().unwrap_or(NULL); + FileChars { + chars, + next, + pos: 0, + } + } + + pub fn next(&mut self) -> char { + let c = self.next; + self.pos += c.len_utf8() as u32; + mem::replace(&mut self.next, self.chars.next().unwrap_or(NULL)) + } + + pub fn eat_while(&mut self, predicate: F) + where + F: Fn(char) -> bool, + { + while predicate(self.next) && self.next != NULL { + self.next(); + } + } +} diff --git a/compiler-rs/src/function_emitter.rs b/compiler-rs/src/function_emitter.rs new file mode 100644 index 0000000..232cb03 --- /dev/null +++ b/compiler-rs/src/function_emitter.rs @@ -0,0 +1,661 @@ +use crate::utils::f64_const; +use crate::{ast::AssignmentOperator, emitter_context::EmitterContext}; +use crate::{ + ast::{ + Assignment, AssignmentTarget, BinaryExpression, BinaryOperator, EelFunction, Expression, + ExpressionBlock, FunctionCall, UnaryExpression, UnaryOperator, + }, + builtin_functions::BuiltinFunction, + constants::BUFFER_SIZE, + error::CompilerError, + shim::Shim, +}; +use crate::{constants::EPSILON, error::EmitterResult}; +use parity_wasm::elements::{BlockType, FuncBody, Instruction, Instructions, Local, ValueType}; + +// https://github.com/WACUP/vis_milk2/blob/de9625a89e724afe23ed273b96b8e48496095b6c/ns-eel2/ns-eel.h#L136 +static MAX_LOOP_COUNT: i32 = 1048576; + +pub fn emit_function( + eel_function: EelFunction, + context: &mut EmitterContext, +) -> EmitterResult { + let mut function_emitter = FunctionEmitter::new(context); + + function_emitter.emit_expression_block(eel_function.expressions)?; + + let mut instructions: Vec = function_emitter.instructions; + instructions.push(Instruction::Drop); + instructions.push(Instruction::End); + + let locals = function_emitter + .locals + .into_iter() + .map(|type_| Local::new(1, type_)) + .collect(); + + Ok(FuncBody::new(locals, Instructions::new(instructions))) +} + +struct FunctionEmitter<'a> { + context: &'a mut EmitterContext, + instructions: Vec, + locals: Vec, +} + +impl<'a> FunctionEmitter<'a> { + fn new(context: &'a mut EmitterContext) -> Self { + Self { + context, + locals: Vec::new(), + instructions: Vec::new(), + } + } + + fn emit_expression_block(&mut self, block: ExpressionBlock) -> EmitterResult<()> { + self.emit_expression_list(block.expressions) + } + + fn emit_expression_list(&mut self, expressions: Vec) -> EmitterResult<()> { + let last_index = expressions.len() - 1; + for (i, expression) in expressions.into_iter().enumerate() { + self.emit_expression(expression)?; + if i != last_index { + self.push(Instruction::Drop) + } + } + Ok(()) + } + + fn emit_expression(&mut self, expression: Expression) -> EmitterResult<()> { + match expression { + Expression::UnaryExpression(unary_expression) => { + self.emit_unary_expression(unary_expression) + } + Expression::BinaryExpression(binary_expression) => { + self.emit_binary_expression(binary_expression) + } + Expression::Assignment(assignment_expression) => { + self.emit_assignment(assignment_expression) + } + Expression::NumberLiteral(number_literal) => { + self.push(Instruction::F64Const(f64_const(number_literal.value))); + Ok(()) + } + Expression::FunctionCall(function_call) => self.emit_function_call(function_call), + Expression::ExpressionBlock(expression_block) => { + self.emit_expression_block(expression_block) + } + Expression::Identifier(identifier) => { + let index = self.context.resolve_variable(identifier.name); + self.push(Instruction::GetGlobal(index)); + Ok(()) + } + } + } + + fn emit_unary_expression(&mut self, unary_expression: UnaryExpression) -> EmitterResult<()> { + match unary_expression.op { + UnaryOperator::Plus => self.emit_expression(*unary_expression.right), + UnaryOperator::Minus => { + self.emit_expression(*unary_expression.right)?; + self.push(Instruction::F64Neg); + Ok(()) + } + UnaryOperator::Not => { + self.emit_expression(*unary_expression.right)?; + self.instructions.extend(vec![ + Instruction::F64Abs, + Instruction::F64Const(f64_const(EPSILON)), + Instruction::F64Lt, + ]); + self.push(Instruction::F64ConvertSI32); + Ok(()) + } + } + } + fn emit_logical_expression( + &mut self, + left: Expression, + right: Expression, + and: bool, + ) -> EmitterResult<()> { + self.emit_expression(left)?; + if and { + self.emit_is_zeroish(); + } else { + self.emit_is_not_zeroish(); + } + self.push(Instruction::If(BlockType::Value(ValueType::F64))); + self.push(Instruction::F64Const(f64_const(if and { + 0.0 + } else { + 1.0 + }))); + self.push(Instruction::Else); + self.emit_expression(right)?; + self.emit_is_not_zeroish(); + self.push(Instruction::F64ConvertSI32); + self.push(Instruction::End); + Ok(()) + } + + fn emit_binary_expression(&mut self, binary_expression: BinaryExpression) -> EmitterResult<()> { + // First we handle cases where we don't just emit arguments in order. + match binary_expression.op { + BinaryOperator::LogicalAnd => { + return self.emit_logical_expression( + *binary_expression.left, + *binary_expression.right, + true, + ); + } + BinaryOperator::LogicalOr => { + return self.emit_logical_expression( + *binary_expression.left, + *binary_expression.right, + false, + ); + } + _ => {} + } + + // Now handle the common cases where we omit arguments in order. + self.emit_expression(*binary_expression.left)?; + self.emit_expression(*binary_expression.right)?; + match binary_expression.op { + BinaryOperator::LogicalAnd | BinaryOperator::LogicalOr => { + // Handled above. Is there was a cleaner way to do this? + } + BinaryOperator::Add => self.push(Instruction::F64Add), + BinaryOperator::Subtract => self.push(Instruction::F64Sub), + BinaryOperator::Multiply => self.push(Instruction::F64Mul), + BinaryOperator::Divide => { + let func_index = self.context.resolve_builtin_function(BuiltinFunction::Div); + self.push(Instruction::Call(func_index)) + } + BinaryOperator::Mod => { + let func_index = self.context.resolve_builtin_function(BuiltinFunction::Mod); + self.push(Instruction::Call(func_index)) + } + BinaryOperator::Eq => { + self.push(Instruction::F64Sub); + self.emit_is_zeroish(); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::NotEqual => { + self.push(Instruction::F64Sub); + self.emit_is_not_zeroish(); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::LessThan => { + self.push(Instruction::F64Lt); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::GreaterThan => { + self.push(Instruction::F64Gt); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::LessThanEqual => { + self.push(Instruction::F64Le); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::GreaterThanEqual => { + self.push(Instruction::F64Ge); + self.push(Instruction::F64ConvertSI32) + } + BinaryOperator::BitwiseAnd => { + let func_index = self + .context + .resolve_builtin_function(BuiltinFunction::BitwiseAnd); + self.push(Instruction::Call(func_index)) + } + BinaryOperator::BitwiseOr => { + let func_index = self + .context + .resolve_builtin_function(BuiltinFunction::BitwiseOr); + self.push(Instruction::Call(func_index)) + } + BinaryOperator::Pow => { + let shim_index = self.context.resolve_shim_function(Shim::Pow); + self.push(Instruction::Call(shim_index)) + } + }; + Ok(()) + } + + fn emit_assignment(&mut self, assignment_expression: Assignment) -> EmitterResult<()> { + let updater: Option = match assignment_expression.operator { + AssignmentOperator::Equal => None, + AssignmentOperator::PlusEqual => Some(Instruction::F64Add), + AssignmentOperator::MinusEqual => Some(Instruction::F64Sub), + AssignmentOperator::TimesEqual => Some(Instruction::F64Mul), + AssignmentOperator::DivEqual => Some(Instruction::F64Div), + AssignmentOperator::ModEqual => { + let mod_index = self.context.resolve_builtin_function(BuiltinFunction::Mod); + Some(Instruction::Call(mod_index)) + } + }; + + match assignment_expression.left { + AssignmentTarget::Identifier(identifier) => { + let resolved_name = self.context.resolve_variable(identifier.name); + match updater { + None => { + self.emit_expression(*assignment_expression.right)?; + + self.push(Instruction::SetGlobal(resolved_name)); + self.push(Instruction::GetGlobal(resolved_name)); + } + Some(update) => { + self.push(Instruction::GetGlobal(resolved_name)); + self.emit_expression(*assignment_expression.right)?; + self.push(update); + self.push(Instruction::SetGlobal(resolved_name)); + self.push(Instruction::GetGlobal(resolved_name)); + } + } + Ok(()) + } + AssignmentTarget::FunctionCall(mut function_call) => { + let memory_offset = match &function_call.name.name[..] { + "megabuf" => 0, + "gmegabuf" => BUFFER_SIZE * 8, + _ => Err(CompilerError::new( + "Only `megabuf()` and `gmegabuf()` can be an assignemnt targets." + .to_string(), + function_call.name.span, + ))?, + }; + assert_arity(&function_call, 1)?; + match updater { + None => { + // TODO: Move this to builtin_functions + let unnormalized_index = self.resolve_local(ValueType::I32); + let right_value = self.resolve_local(ValueType::F64); + + let index = function_call.arguments.pop().unwrap(); + // Emit the right hand side unconditionally to ensure it always runs. + self.emit_expression(*assignment_expression.right)?; + self.push(Instruction::SetLocal(right_value)); + self.emit_expression(index)?; + let get_buffer_index = self + .context + .resolve_builtin_function(BuiltinFunction::GetBufferIndex); + self.push(Instruction::Call(get_buffer_index)); + self.push(Instruction::TeeLocal(unnormalized_index)); + self.push(Instruction::I32Const(0)); + self.push(Instruction::I32LtS); + // STACK: [is the index out of range?] + self.push(Instruction::If(BlockType::Value(ValueType::F64))); + self.push(Instruction::F64Const(f64_const(0.0))); + self.push(Instruction::Else); + self.push(Instruction::GetLocal(unnormalized_index)); + self.push(Instruction::TeeLocal(unnormalized_index)); + // STACK: [buffer index] + self.push(Instruction::GetLocal(right_value)); + // STACK: [buffer index, right] + self.push(Instruction::F64Store(3, memory_offset)); + // STACK: [] + self.push(Instruction::GetLocal(right_value)); + // STACK: [Right/Buffer value] + self.push(Instruction::End); + Ok(()) + } + Some(update) => { + // TODO: Move this to wasmFunctions once we know how to call functions + // from within functions (need to get the offset). + let index = self.resolve_local(ValueType::I32); + let in_bounds = self.resolve_local(ValueType::I32); + let right_value = self.resolve_local(ValueType::F64); + let result = self.resolve_local(ValueType::F64); + let left = function_call.arguments.pop().unwrap(); + self.emit_expression(*assignment_expression.right)?; + self.push(Instruction::SetLocal(right_value)); + self.emit_expression(left)?; + let get_buffer_index = self + .context + .resolve_builtin_function(BuiltinFunction::GetBufferIndex); + self.push(Instruction::Call(get_buffer_index)); + self.push(Instruction::TeeLocal(index)); + // STACK: [index] + self.push(Instruction::I32Const(-1)); + self.push(Instruction::I32Ne); + self.push(Instruction::TeeLocal(in_bounds)); + self.push(Instruction::If(BlockType::Value(ValueType::F64))); + self.push(Instruction::GetLocal(index)); + self.push(Instruction::F64Load(3, memory_offset)); + self.push(Instruction::Else); + self.push(Instruction::F64Const(f64_const(0.0))); + self.push(Instruction::End); + // STACK: [current value from memory || 0] + // Apply the mutation + self.push(Instruction::GetLocal(right_value)); + self.push(update); + self.push(Instruction::TeeLocal(result)); + self.push(Instruction::GetLocal(in_bounds)); + // STACK: [new value] + self.push(Instruction::If(BlockType::NoResult)); + self.push(Instruction::GetLocal(index)); + self.push(Instruction::GetLocal(result)); + self.push(Instruction::F64Store(3, memory_offset)); + self.push(Instruction::End); + Ok(()) + } + } + } + } + } + + fn emit_function_call(&mut self, mut function_call: FunctionCall) -> EmitterResult<()> { + match &function_call.name.name[..] { + "int" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Floor); + } + "if" => { + assert_arity(&function_call, 3)?; + + let alternate = function_call.arguments.pop().unwrap(); + let consiquent = function_call.arguments.pop().unwrap(); + let test = function_call.arguments.pop().unwrap(); + + self.emit_expression(test)?; + self.emit_is_not_zeroish(); + self.push(Instruction::If(BlockType::Value(ValueType::F64))); + self.emit_expression(consiquent)?; + self.push(Instruction::Else); + self.emit_expression(alternate)?; + self.push(Instruction::End); + } + "abs" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Abs) + } + "sqrt" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Abs); + self.push(Instruction::F64Sqrt) + } + "min" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Min) + } + "max" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Max) + } + "above" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Gt); + self.push(Instruction::F64ConvertSI32) + } + "below" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Lt); + self.push(Instruction::F64ConvertSI32) + } + "equal" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Sub); + self.emit_is_zeroish(); + self.push(Instruction::F64ConvertSI32) + } + "bnot" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.emit_is_zeroish(); + self.push(Instruction::F64ConvertSI32) + } + "floor" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Floor) + } + "ceil" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + + self.push(Instruction::F64Ceil) + } + "sqr" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + let func_index = self.context.resolve_builtin_function(BuiltinFunction::Sqr); + + self.push(Instruction::Call(func_index)) + } + "bor" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + let func_index = self + .context + .resolve_builtin_function(BuiltinFunction::LogicalOr); + + self.push(Instruction::Call(func_index)) + } + "band" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + let func_index = self + .context + .resolve_builtin_function(BuiltinFunction::LogicalAnd); + + self.push(Instruction::Call(func_index)) + } + // TODO: Add a test for this + "mod" => { + assert_arity(&function_call, 2)?; + self.emit_function_args(function_call)?; + let func_index = self.context.resolve_builtin_function(BuiltinFunction::Mod); + + self.push(Instruction::Call(func_index)) + } + "sign" => { + assert_arity(&function_call, 1)?; + self.emit_function_args(function_call)?; + let func_index = self.context.resolve_builtin_function(BuiltinFunction::Sign); + + self.push(Instruction::Call(func_index)) + } + "exec2" => { + assert_arity(&function_call, 2)?; + self.emit_expression_list(function_call.arguments)? + } + "exec3" => { + assert_arity(&function_call, 3)?; + self.emit_expression_list(function_call.arguments)? + } + "while" => { + assert_arity(&function_call, 1)?; + let body = function_call.arguments.pop().unwrap(); + self.emit_while(body)?; + } + "loop" => { + assert_arity(&function_call, 2)?; + let body = function_call.arguments.pop().unwrap(); + let count = function_call.arguments.pop().unwrap(); + self.emit_loop(count, body)?; + } + "assign" => { + assert_arity(&function_call, 2)?; + let value = function_call.arguments.pop().unwrap(); + let variable = function_call.arguments.pop().unwrap(); + if let Expression::Identifier(identifier) = variable { + let resolved_name = self.context.resolve_variable(identifier.name); + self.emit_expression(value)?; + self.push(Instruction::SetGlobal(resolved_name)); + self.push(Instruction::GetGlobal(resolved_name)); + } else { + Err(CompilerError::new( + "Expected the first argument of `assign()` to be an identifier." + .to_string(), + // TODO: Point this to the first arg + function_call.name.span, + ))? + } + } + "megabuf" => self.emit_memory_access(&mut function_call, 0)?, + "gmegabuf" => self.emit_memory_access(&mut function_call, BUFFER_SIZE * 8)?, + shim_name if Shim::from_str(shim_name).is_some() => { + let shim = Shim::from_str(shim_name).unwrap(); + assert_arity(&function_call, shim.arity())?; + self.emit_function_args(function_call)?; + + let shim_index = self.context.resolve_shim_function(shim); + self.push(Instruction::Call(shim_index)); + } + _ => { + return Err(CompilerError::new( + format!("Unknown function `{}`", function_call.name.name), + function_call.name.span, + )) + } + } + Ok(()) + } + fn emit_function_args(&mut self, function_call: FunctionCall) -> EmitterResult<()> { + for arg in function_call.arguments { + self.emit_expression(arg)?; + } + Ok(()) + } + + fn emit_memory_access( + &mut self, + function_call: &mut FunctionCall, + memory_offset: u32, + ) -> EmitterResult<()> { + assert_arity(&function_call, 1)?; + let index = self.resolve_local(ValueType::I32); + self.emit_expression(function_call.arguments.pop().unwrap())?; + + let call_index = self + .context + .resolve_builtin_function(BuiltinFunction::GetBufferIndex); + self.push(Instruction::Call(call_index)); + // + self.push(Instruction::TeeLocal(index)); + self.push(Instruction::I32Const(-1)); + self.push(Instruction::I32Ne); + // STACK: [in range] + self.push(Instruction::If(BlockType::Value(ValueType::F64))); + self.push(Instruction::GetLocal(index)); + self.push(Instruction::F64Load(3, memory_offset)); + self.push(Instruction::Else); + self.push(Instruction::F64Const(f64_const(0.0))); + self.push(Instruction::End); + + Ok(()) + } + + fn emit_while(&mut self, body: Expression) -> EmitterResult<()> { + let iteration_idx = self.resolve_local(ValueType::I32); + self.push(Instruction::I32Const(0)); + self.push(Instruction::SetLocal(iteration_idx)); + + self.push(Instruction::Loop(BlockType::NoResult)); + + // Increment and check loop count + self.push(Instruction::GetLocal(iteration_idx)); + self.push(Instruction::I32Const(1)); + self.push(Instruction::I32Add); + self.push(Instruction::TeeLocal(iteration_idx)); + // STACK: [iteration count] + self.push(Instruction::I32Const(MAX_LOOP_COUNT)); + self.push(Instruction::I32LtU); + // STACK: [loop in range] + self.emit_expression(body)?; + self.emit_is_not_zeroish(); + // STACK: [loop in range, body is truthy] + self.push(Instruction::I32And); + self.push(Instruction::BrIf(0)); + self.push(Instruction::End); + self.push(Instruction::F64Const(f64_const(0.0))); + Ok(()) + } + + fn emit_loop(&mut self, count: Expression, body: Expression) -> EmitterResult<()> { + let iteration_idx = self.resolve_local(ValueType::I32); + self.push(Instruction::Block(BlockType::NoResult)); + // Assign the count to a variable + self.emit_expression(count)?; + self.push(Instruction::I32TruncSF64); + self.push(Instruction::TeeLocal(iteration_idx)); + self.push(Instruction::I32Const(0)); + self.push(Instruction::I32LeS); + self.push(Instruction::BrIf(1)); + self.push(Instruction::Loop(BlockType::NoResult)); + // Run the body + self.emit_expression(body)?; + self.push(Instruction::Drop); + // Decrement the count + self.push(Instruction::GetLocal(iteration_idx)); + self.push(Instruction::I32Const(1)); + self.push(Instruction::I32Sub); + self.push(Instruction::TeeLocal(iteration_idx)); + self.push(Instruction::I32Const(0)); + self.push(Instruction::I32Ne); + self.push(Instruction::BrIf(0)); // Return to the top of the loop + self.push(Instruction::End); // End loop + self.push(Instruction::End); // End block + self.push(Instruction::F64Const(f64_const(0.0))); // Implicitly return zero + Ok(()) + } + + fn push(&mut self, instruction: Instruction) { + self.instructions.push(instruction) + } + + fn emit_is_not_zeroish(&mut self) { + self.push(Instruction::F64Abs); + self.push(Instruction::F64Const(f64_const(EPSILON))); + self.push(Instruction::F64Gt); + } + + fn emit_is_zeroish(&mut self) { + self.push(Instruction::F64Abs); + self.push(Instruction::F64Const(f64_const(EPSILON))); + self.push(Instruction::F64Lt); + } + + fn resolve_local(&mut self, type_: ValueType) -> u32 { + self.locals.push(type_); + return self.locals.len() as u32 - 1; + } +} + +fn assert_arity(function_call: &FunctionCall, arity: usize) -> EmitterResult<()> { + if function_call.arguments.len() != arity { + Err(CompilerError::new( + format!( + "Incorrect argument count for function `{}`. Expected {} but got {}.", + function_call.name.name, + arity, + function_call.arguments.len() + ), + // TODO: Better to underline the argument list + function_call.name.span, + )) + } else { + Ok(()) + } +} diff --git a/compiler-rs/src/index_store.rs b/compiler-rs/src/index_store.rs new file mode 100644 index 0000000..3778984 --- /dev/null +++ b/compiler-rs/src/index_store.rs @@ -0,0 +1,38 @@ +use indexmap::map::Entry; +use indexmap::IndexMap; +use std::hash::Hash; + +#[derive(Default)] +pub struct IndexStore { + map: IndexMap, +} + +impl IndexStore { + pub fn new() -> Self { + IndexStore { + map: IndexMap::new(), + } + } + pub fn get(&mut self, key: T) -> u32 { + let next = self.map.len() as u32; + match self.map.entry(key) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + entry.insert(next); + next + } + } + } + + pub fn ensure(&mut self, key: T) { + let next = self.map.len() as u32; + if let Entry::Vacant(entry) = self.map.entry(key) { + entry.insert(next); + } + } + + // TODO: Return iter? + pub fn keys(&self) -> Vec<&T> { + self.map.keys().collect() + } +} diff --git a/compiler-rs/src/lexer.rs b/compiler-rs/src/lexer.rs new file mode 100644 index 0000000..bfaa865 --- /dev/null +++ b/compiler-rs/src/lexer.rs @@ -0,0 +1,281 @@ +use crate::error::CompilerError; + +use super::file_chars::{FileChars, NULL}; +use super::span::Span; +use super::tokens::{Token, TokenKind}; + +/** + * The lexer: + * - Returns an error if next token is not in the language + * - Returns EOF forever once it reaches the end + */ + +type LexerResult = Result; + +pub struct Lexer<'a> { + source: &'a str, + chars: FileChars<'a>, +} + +// TODO: Consider https://github.com/maciejhirsz/logos +impl<'a> Lexer<'a> { + pub fn new(source: &'a str) -> Self { + Lexer { + source, + chars: FileChars::new(source), + } + } + + pub fn next_token(&mut self) -> LexerResult { + let start = self.chars.pos; + let kind = match self.chars.next { + c if is_int(c) => self.read_number(), + '.' => self.read_number(), + c if is_identifier_head(c) => self.read_identifier(), + '+' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::PlusEqual), + _ => TokenKind::Plus, + } + } + '-' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::MinusEqual), + _ => TokenKind::Minus, + } + } + '*' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::TimesEqual), + _ => TokenKind::Asterisk, + } + } + '/' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::DivEqual), + '/' => { + self.chars.next(); + self.eat_inline_comment_tail(); + return self.next_token(); + } + '*' => { + self.chars.next(); + self.eat_block_comment_tail(start)?; + return self.next_token(); + } + _ => TokenKind::Slash, + } + } + // Eel supports backslash comments! + '\\' => { + self.chars.next(); + match self.chars.next { + '\\' => { + self.chars.next(); + self.eat_inline_comment_tail(); + return self.next_token(); + } + c => { + return Err(CompilerError::new( + format!("Parse Error: Unexpected character '{}' following \\.", c), + Span::new(start, start), + )) + } + } + } + '=' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::DoubleEqual), + _ => TokenKind::Equal, + } + } + '(' => self.read_char_as_kind(TokenKind::OpenParen), + ')' => self.read_char_as_kind(TokenKind::CloseParen), + ',' => self.read_char_as_kind(TokenKind::Comma), + ';' => self.read_char_as_kind(TokenKind::Semi), + '!' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::NotEqual), + _ => TokenKind::Bang, + } + } + '%' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::ModEqual), + _ => TokenKind::Percent, + } + } + '&' => { + self.chars.next(); + match self.chars.next { + '&' => self.read_char_as_kind(TokenKind::AndAnd), + _ => TokenKind::And, + } + } + '|' => { + self.chars.next(); + match self.chars.next { + '|' => self.read_char_as_kind(TokenKind::PipePipe), + _ => TokenKind::Pipe, + } + } + '^' => self.read_char_as_kind(TokenKind::Caret), + '<' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::LTEqual), + _ => TokenKind::OpenAngel, + } + } + '>' => { + self.chars.next(); + match self.chars.next { + '=' => self.read_char_as_kind(TokenKind::GTEqual), + _ => TokenKind::CloseAngel, + } + } + c if is_whitepsace(c) => { + self.chars.eat_while(is_whitepsace); + return self.next_token(); + } + NULL => TokenKind::EOF, + c => { + return Err(CompilerError::new( + format!("Parse Error: Unexpected character '{}'.", c), + Span::new(start, start), + )) + } + }; + let end = self.chars.pos; + Ok(Token::new(kind, Span::new(start, end))) + } + + fn read_char_as_kind(&mut self, kind: TokenKind) -> TokenKind { + self.chars.next(); + kind + } + + fn read_number(&mut self) -> TokenKind { + if is_int(self.chars.next) { + self.chars.next(); + self.chars.eat_while(is_int); + } + if self.chars.next == '.' { + self.chars.next(); + self.chars.eat_while(is_int); + } + TokenKind::Int + } + + fn read_identifier(&mut self) -> TokenKind { + self.chars.next(); + self.chars.eat_while(is_identifier_tail); + TokenKind::Identifier + } + + fn eat_block_comment_tail(&mut self, start: u32) -> LexerResult<()> { + loop { + self.chars.eat_while(|c| c != '*'); + self.chars.next(); + if self.chars.next == NULL { + Err(CompilerError::new( + format!("Unclosed block comment."), + Span::new(start, self.chars.pos), + ))?; + } else if self.chars.next == '/' { + self.chars.next(); + break; + }; + } + Ok(()) + } + + fn eat_inline_comment_tail(&mut self) { + self.chars.eat_while(not_line_break); + self.chars.next(); + } + + pub fn source(&self, span: Span) -> &str { + &self.source[span.start as usize..span.end as usize] + } +} + +// https://github.com/justinfrankel/WDL/blob/63943fbac273b847b733aceecdb16703679967b9/WDL/eel2/eel2.l#L93 +fn is_identifier_head(c: char) -> bool { + match c { + 'a'..='z' | 'A'..='Z' | '_' => true, + _ => false, + } +} + +// https://github.com/justinfrankel/WDL/blob/63943fbac273b847b733aceecdb16703679967b9/WDL/eel2/eel2.l#L93 +fn is_identifier_tail(c: char) -> bool { + match c { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => true, + _ => false, + } +} + +fn is_int(c: char) -> bool { + match c { + '0'..='9' => true, + _ => false, + } +} + +fn is_whitepsace(c: char) -> bool { + c.is_whitespace() +} + +fn not_line_break(c: char) -> bool { + match c { + // TODO: Windows line endings? + '\n' => false, + _ => true, + } +} + +#[test] +fn can_lex_number() { + let mut lexer = Lexer::new("1"); + let mut token_kinds: Vec = vec![]; + loop { + let token = lexer.next_token().expect("token"); + let done = token.kind == TokenKind::EOF; + token_kinds.push(token.kind); + if done { + break; + } + } + assert_eq!(token_kinds, vec![TokenKind::Int, TokenKind::EOF]); +} + +#[test] +fn can_lex_assignment() { + let mut lexer = Lexer::new("g=1"); + let mut token_kinds: Vec = vec![]; + loop { + let token = lexer.next_token().expect("token"); + let done = token.kind == TokenKind::EOF; + token_kinds.push(token.kind); + if done { + break; + } + } + assert_eq!( + token_kinds, + vec![ + TokenKind::Identifier, + TokenKind::Equal, + TokenKind::Int, + TokenKind::EOF + ] + ); +} diff --git a/compiler-rs/src/lib.rs b/compiler-rs/src/lib.rs new file mode 100644 index 0000000..e69f822 --- /dev/null +++ b/compiler-rs/src/lib.rs @@ -0,0 +1,61 @@ +mod ast; +mod builtin_functions; +mod constants; +mod emitter_context; +mod error; +mod file_chars; +mod function_emitter; +mod index_store; +mod lexer; +mod module_emitter; +mod parser; +mod shim; +mod span; +mod tokens; +mod utils; + +use std::collections::{HashMap, HashSet}; + +use ast::EelFunction; +use error::CompilerError; +use module_emitter::emit_module; +// Only exported for tests +pub use lexer::Lexer; +use parity_wasm::elements::FunctionType; +pub use parser::parse; +pub use shim::Shim; +pub use tokens::Token; +pub use tokens::TokenKind; + +pub type EelFunctionType = FunctionType; + +use wasm_bindgen::prelude::*; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +pub fn assert_compile(source: &str) -> Vec { + compile( + vec![("test".to_string(), source, "pool".to_string())], + HashMap::default(), + ) + .expect("Don't screw it up") +} + +pub fn compile( + sources: Vec<(String, &str, String)>, + globals: HashMap>, +) -> Result, CompilerError> { + let eel_functions: Result, CompilerError> = sources + .into_iter() + .map(|(name, source, pool)| { + let program = parse(&source)?; + Ok((name, program, pool)) + }) + .collect(); + emit_module(eel_functions?, globals) +} diff --git a/compiler-rs/src/module_emitter.rs b/compiler-rs/src/module_emitter.rs new file mode 100644 index 0000000..447feee --- /dev/null +++ b/compiler-rs/src/module_emitter.rs @@ -0,0 +1,225 @@ +use crate::emitter_context::{EmitterContext, ModuleFunction}; +use std::collections::{HashMap, HashSet}; + +use crate::{ + ast::EelFunction, + constants::WASM_MEMORY_SIZE, + error::{CompilerError, EmitterResult}, + function_emitter::emit_function, + shim::Shim, + span::Span, + utils::f64_const, +}; +use parity_wasm::elements::{ + CodeSection, ExportEntry, ExportSection, External, Func, FuncBody, FunctionSection, + FunctionType, GlobalEntry, GlobalSection, GlobalType, ImportEntry, ImportSection, InitExpr, + Instruction, Internal, MemorySection, MemoryType, Module, Section, Serialize, Type, + TypeSection, ValueType, +}; + +pub fn emit_module( + eel_functions: Vec<(String, EelFunction, String)>, + globals_map: HashMap>, +) -> EmitterResult> { + let mut emitter = Emitter::new(); + emitter.emit(eel_functions, globals_map) +} + +struct Emitter { + context: EmitterContext, +} + +impl Emitter { + fn new() -> Self { + Emitter { + context: EmitterContext::new(), + } + } + fn emit( + &mut self, + eel_functions: Vec<(String, EelFunction, String)>, + globals_map: HashMap>, // HahsMap> + ) -> EmitterResult> { + let mut imports = Vec::new(); + + for (pool_name, globals) in globals_map { + self.context.current_pool = pool_name; + for global in globals { + // TODO: Ensure none of these are `ref\d\d` + // TODO: Lots of clones. + self.context.resolve_variable(global.clone()); + imports.push(make_import_entry( + self.context.current_pool.clone(), + global.clone(), + )); + } + } + + for shim in Shim::all() { + let field_str = shim.as_str().to_string(); + let type_ = shim.get_type(); + self.context.resolve_shim_function(shim); + imports.push(ImportEntry::new( + "shims".to_string(), + field_str, + External::Function(self.context.function_types.get(type_)), + )); + } + + let (function_exports, function_bodies, funcs) = self.emit_eel_functions(eel_functions)?; + + let mut sections = vec![]; + sections.push(Section::Type(self.emit_type_section())); + if let Some(import_section) = self.emit_import_section(imports) { + sections.push(Section::Import(import_section)); + } + sections.push(Section::Function(self.emit_function_section(funcs))); + + sections.push(Section::Memory(MemorySection::with_entries(vec![ + MemoryType::new(WASM_MEMORY_SIZE, Some(WASM_MEMORY_SIZE)), + ]))); + + if let Some(global_section) = self.emit_global_section() { + sections.push(Section::Global(global_section)); + } + sections.push(Section::Export(self.emit_export_section(function_exports))); + sections.push(Section::Code(self.emit_code_section(function_bodies))); + + let mut binary: Vec = Vec::new(); + Module::new(sections) + .serialize(&mut binary) + .map_err(|err| { + CompilerError::new( + format!("Module serialization error: {}", err), + Span::empty(), + ) + })?; + + Ok(binary) + } + + fn emit_type_section(&self) -> TypeSection { + let function_types = self + .context + .function_types + .keys() + .into_iter() + .map(|function_type| { + Type::Function(FunctionType::new( + // TODO: This is clone with more steps. What's going on + function_type.params().to_vec(), + function_type.results().to_vec(), + )) + }) + .collect(); + TypeSection::with_types(function_types) + } + + fn emit_import_section(&self, imports: Vec) -> Option { + if imports.len() > 0 { + Some(ImportSection::with_entries(imports)) + } else { + None + } + } + + fn emit_function_section(&mut self, funcs: Vec) -> FunctionSection { + let mut entries = Vec::new(); + for module_function in self.context.functions.keys() { + let func = match module_function { + ModuleFunction::Shim(_) => None, + ModuleFunction::Builtin(builtin) => { + let type_idx = self.context.function_types.get(builtin.get_type()); + Some(Func::new(type_idx)) + } + ModuleFunction::Eel(func_idx) => Some(funcs.get(*func_idx).unwrap().clone()), + }; + if let Some(func) = func { + entries.push(func); + } + } + FunctionSection::with_entries(entries) + } + + fn emit_global_section(&self) -> Option { + let globals: Vec = self + .context + .globals + .keys() + .iter() + .map(|_| make_empty_global()) + .collect(); + + if globals.len() == 0 { + None + } else { + Some(GlobalSection::with_entries(globals)) + } + } + + fn emit_export_section(&self, function_exports: Vec) -> ExportSection { + ExportSection::with_entries(function_exports) + } + + fn emit_code_section(&self, function_bodies: Vec) -> CodeSection { + let mut bodies = Vec::new(); + for module_function in self.context.functions.keys() { + let body = match module_function { + ModuleFunction::Shim(_) => None, + ModuleFunction::Builtin(builtin) => Some(builtin.func_body(&self.context)), + ModuleFunction::Eel(func_idx) => { + // TODO: Avoid this clone + Some(function_bodies.get(*func_idx).unwrap().clone()) + } + }; + if let Some(body) = body { + bodies.push(body); + } + } + CodeSection::with_bodies(bodies) + } + + fn emit_eel_functions( + &mut self, + eel_functions: Vec<(String, EelFunction, String)>, + ) -> EmitterResult<(Vec, Vec, Vec)> { + let mut exports = Vec::new(); + let mut function_bodies = Vec::new(); + let mut function_definitions = Vec::new(); + for (i, (name, program, pool_name)) in eel_functions.into_iter().enumerate() { + let function_idx = self.context.resolve_eel_function(i); + self.context.current_pool = pool_name; + let function_body = emit_function(program, &mut self.context)?; + + function_bodies.push(function_body); + + exports.push(ExportEntry::new(name, Internal::Function(function_idx))); + + let function_type = self + .context + .function_types + .get(FunctionType::new(vec![], vec![])); + + function_definitions.push(Func::new(function_type)) + } + Ok((exports, function_bodies, function_definitions)) + } +} + +fn make_empty_global() -> GlobalEntry { + GlobalEntry::new( + GlobalType::new(ValueType::F64, true), + InitExpr::new(vec![ + Instruction::F64Const(f64_const(0.0)), + Instruction::End, + ]), + ) +} + +fn make_import_entry(module_str: String, field_str: String) -> ImportEntry { + ImportEntry::new( + module_str, + field_str, + External::Global(GlobalType::new(ValueType::F64, true)), + ) +} diff --git a/compiler-rs/src/parser.rs b/compiler-rs/src/parser.rs new file mode 100644 index 0000000..4d1fe29 --- /dev/null +++ b/compiler-rs/src/parser.rs @@ -0,0 +1,388 @@ +use std::num::ParseFloatError; + +use crate::ast::{ + Assignment, AssignmentOperator, AssignmentTarget, BinaryExpression, BinaryOperator, + ExpressionBlock, FunctionCall, Identifier, UnaryExpression, UnaryOperator, +}; + +use super::ast::{EelFunction, Expression, NumberLiteral}; +use super::error::CompilerError; +use super::lexer::Lexer; +use super::span::Span; +use super::tokens::{Token, TokenKind}; + +// https://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/ +static ASSIGNMENT_PRECEDENCE: u8 = 1; +// static CONDITIONAL_PRECEDENCE: u8 = 2; +static SUM_PRECEDENCE: u8 = 3; +static DIFFERENCE_PRECEDENCE: u8 = 3; +static PRODUCT_PRECEDENCE: u8 = 4; +static QUOTIENT_PRECEDENCE: u8 = 4; +static EXPONENTIATION_PRECEDENCE: u8 = 5; +static MOD_PRECEDENCE: u8 = 5; // A little strange, in JS this would match product/quotient +static PREFIX_PRECEDENCE: u8 = 6; +// static POSTFIX_PRECEDENCE: u8 = 7; +// static CALL_PRECEDENCE: u8 = 8; + +struct Parser<'a> { + lexer: Lexer<'a>, + token: Token, +} + +type ParseResult = Result; + +pub fn parse(src: &str) -> ParseResult { + let mut parser = Parser::new(&src); + parser.parse() +} + +impl<'a> Parser<'a> { + pub fn new(source: &'a str) -> Self { + Parser { + lexer: Lexer::new(source), + token: Token { + kind: TokenKind::SOF, + span: Span::empty(), + }, + } + } + + fn advance(&mut self) -> ParseResult<()> { + self.token = self.lexer.next_token()?; + Ok(()) + } + + fn expect_kind(&mut self, expected: TokenKind) -> ParseResult<()> { + let token = self.peek(); + if token.kind == expected { + self.advance()?; + Ok(()) + } else { + Err(CompilerError::new( + format!("Expected a {:?} but found {:?}", expected, self.token.kind), + token.span, + )) + } + } + + pub fn parse(&mut self) -> ParseResult { + self.expect_kind(TokenKind::SOF)?; + let program = self.parse_program()?; + self.expect_kind(TokenKind::EOF)?; + Ok(program) + } + + pub fn parse_program(&mut self) -> ParseResult { + Ok(EelFunction { + expressions: self.parse_expression_block()?, + }) + } + + pub fn parse_expression_block(&mut self) -> ParseResult { + let mut expressions = vec![]; + while self.peek_expression() { + expressions.push(self.parse_expression(0)?); + // TODO: This is probably not quite right. We should require semis between expressions. + while self.peek().kind == TokenKind::Semi { + self.advance()?; + } + } + Ok(ExpressionBlock { expressions }) + } + + fn peek_expression(&self) -> bool { + self.peek_prefix() + } + + fn parse_expression(&mut self, precedence: u8) -> ParseResult { + let left = self.parse_prefix()?; + self.maybe_parse_infix(left, precedence) + } + + fn peek_prefix(&self) -> bool { + let token = self.peek(); + match token.kind { + TokenKind::OpenParen => true, + TokenKind::Int => true, + TokenKind::Plus => true, + TokenKind::Minus => true, + TokenKind::Bang => true, + TokenKind::Identifier => true, + _ => false, + } + } + + fn parse_prefix(&mut self) -> ParseResult { + match self.token.kind { + TokenKind::OpenParen => { + self.advance()?; + let expression_block = self.parse_expression_block()?; + self.expect_kind(TokenKind::CloseParen)?; + Ok(Expression::ExpressionBlock(expression_block)) + } + TokenKind::Int => Ok(Expression::NumberLiteral(self.parse_int()?)), + TokenKind::Plus => { + self.advance()?; + Ok(Expression::UnaryExpression(UnaryExpression { + right: Box::new(self.parse_expression(PREFIX_PRECEDENCE)?), + op: UnaryOperator::Plus, + })) + } + TokenKind::Minus => { + self.advance()?; + Ok(Expression::UnaryExpression(UnaryExpression { + right: Box::new(self.parse_expression(PREFIX_PRECEDENCE)?), + op: UnaryOperator::Minus, + })) + } + TokenKind::Bang => { + self.advance()?; + Ok(Expression::UnaryExpression(UnaryExpression { + right: Box::new(self.parse_expression(PREFIX_PRECEDENCE)?), + op: UnaryOperator::Not, + })) + } + // TokenKind::OpenParen => self.parse_parenthesized_expression(), + // Once we have other prefix operators: `+-!` they will go here. + TokenKind::Identifier => self.parse_identifier_expression(), + _ => Err(CompilerError::new( + format!("Expected Int or Identifier but got {:?}", self.token.kind), + self.token.span, + )), + } + } + + fn maybe_parse_infix(&mut self, left: Expression, precedence: u8) -> ParseResult { + let mut next = left; + loop { + let (precedence, op) = match self.token.kind { + TokenKind::Plus if precedence < SUM_PRECEDENCE => { + (left_associative(SUM_PRECEDENCE), BinaryOperator::Add) + } + TokenKind::Minus if precedence < DIFFERENCE_PRECEDENCE => ( + left_associative(DIFFERENCE_PRECEDENCE), + BinaryOperator::Subtract, + ), + TokenKind::Asterisk if precedence < PRODUCT_PRECEDENCE => ( + left_associative(PRODUCT_PRECEDENCE), + BinaryOperator::Multiply, + ), + TokenKind::Slash if precedence < QUOTIENT_PRECEDENCE => ( + left_associative(QUOTIENT_PRECEDENCE), + BinaryOperator::Divide, + ), + TokenKind::Percent if precedence < MOD_PRECEDENCE => { + (left_associative(MOD_PRECEDENCE), BinaryOperator::Mod) + } + // TODO: prededence? + TokenKind::DoubleEqual if precedence < ASSIGNMENT_PRECEDENCE => { + (left_associative(ASSIGNMENT_PRECEDENCE), BinaryOperator::Eq) + } + TokenKind::OpenAngel => (0, BinaryOperator::LessThan), + TokenKind::CloseAngel => (0, BinaryOperator::GreaterThan), + TokenKind::LTEqual => (0, BinaryOperator::LessThanEqual), + TokenKind::GTEqual => (0, BinaryOperator::GreaterThanEqual), + TokenKind::NotEqual => (0, BinaryOperator::NotEqual), + TokenKind::And => (0, BinaryOperator::BitwiseAnd), + TokenKind::AndAnd => (0, BinaryOperator::LogicalAnd), + TokenKind::PipePipe => (0, BinaryOperator::LogicalOr), + TokenKind::Pipe => (0, BinaryOperator::BitwiseOr), + TokenKind::Caret if precedence < EXPONENTIATION_PRECEDENCE => ( + left_associative(EXPONENTIATION_PRECEDENCE), + BinaryOperator::Pow, + ), + _ => return Ok(next), + }; + + self.advance()?; + + let right = self.parse_expression(precedence)?; + next = Expression::BinaryExpression(BinaryExpression { + left: Box::new(next), + right: Box::new(right), + op, + }); + } + } + + fn parse_int(&mut self) -> ParseResult { + if let TokenKind::Int = self.token.kind { + let value = self.lexer.source(self.token.span); + match parse_number(value) { + Ok(value) => { + self.advance()?; + Ok(NumberLiteral { value }) + } + Err(_) => Err(CompilerError::new( + format!("Could not parse \"{}\" to a number", value), + self.token.span, + )), + } + } else { + Err(CompilerError::new( + format!("Expected an Int but found {:?}", self.token.kind), + self.token.span, + )) + } + } + + fn parse_identifier(&mut self) -> ParseResult { + let span = self.token.span; + self.expect_kind(TokenKind::Identifier)?; + Ok(Identifier { + name: self.lexer.source(span).to_lowercase(), + span, + }) + } + + fn parse_identifier_assignment( + &mut self, + target: AssignmentTarget, + operator: AssignmentOperator, + ) -> ParseResult { + self.advance()?; + let right = self.parse_expression(0)?; + Ok(Expression::Assignment(Assignment { + left: target, + operator, + right: Box::new(right), + })) + } + + fn parse_assignment_tail(&mut self, left: AssignmentTarget) -> ParseResult { + match self.token.kind { + TokenKind::Equal => self.parse_identifier_assignment(left, AssignmentOperator::Equal), + TokenKind::PlusEqual => { + self.parse_identifier_assignment(left, AssignmentOperator::PlusEqual) + } + TokenKind::MinusEqual => { + self.parse_identifier_assignment(left, AssignmentOperator::MinusEqual) + } + TokenKind::TimesEqual => { + self.parse_identifier_assignment(left, AssignmentOperator::TimesEqual) + } + TokenKind::DivEqual => { + self.parse_identifier_assignment(left, AssignmentOperator::DivEqual) + } + TokenKind::ModEqual => { + self.parse_identifier_assignment(left, AssignmentOperator::ModEqual) + } + _ => { + // If you hit this, peek_assignment is wrong. + Err(CompilerError::new( + "Unexpected assignment token".to_string(), + Span::empty(), + )) + } + } + } + + fn peek_assignment(&self) -> bool { + match self.token.kind { + TokenKind::Equal + | TokenKind::PlusEqual + | TokenKind::MinusEqual + | TokenKind::TimesEqual + | TokenKind::DivEqual + | TokenKind::ModEqual => true, + _ => false, + } + } + + fn parse_function_call(&mut self, name: Identifier) -> ParseResult { + self.advance()?; + let mut arguments = vec![]; + while self.peek_expression() { + let mut block = self.parse_expression_block()?; + // Janky here. Some functions are special and expect the arguments to be specific kinds of + // expressions. The possiblity of an ExpressionBlock wrapper complicates those checks, so we + // avoid the wrapper in the common case of a single expression. + if block.expressions.len() == 1 { + arguments.push(block.expressions.pop().unwrap()); + } else { + arguments.push(Expression::ExpressionBlock(block)); + } + match self.peek().kind { + TokenKind::Comma => self.advance()?, + TokenKind::CloseParen => { + self.advance()?; + break; + } + _ => { + return Err(CompilerError::new( + "Expected , or )".to_string(), + self.token.span, + )) + } + } + } + let function_call = FunctionCall { name, arguments }; + + if self.peek_assignment() { + self.parse_assignment_tail(AssignmentTarget::FunctionCall(function_call)) + } else { + Ok(Expression::FunctionCall(function_call)) + } + } + + fn parse_identifier_expression(&mut self) -> ParseResult { + let identifier = self.parse_identifier()?; + + if self.peek_assignment() { + return self.parse_assignment_tail(AssignmentTarget::Identifier(identifier)); + } + + match &self.token.kind { + TokenKind::OpenParen => self.parse_function_call(identifier), + _ => Ok(Expression::Identifier(identifier)), + } + } + + fn peek(&self) -> &Token { + &self.token + } +} + +fn parse_number(raw: &str) -> Result { + if raw.starts_with('.') { + format!("0{}", raw).parse::() + } else { + raw.parse::() + } +} + +#[inline] +#[allow(dead_code)] // Save this for when we need it. +fn left_associative(precedence: u8) -> u8 { + precedence +} + +#[inline] +#[allow(dead_code)] // Save this for when we need it. +fn right_associative(precedence: u8) -> u8 { + precedence - 1 +} + +#[test] +fn can_parse_integer() { + assert_eq!( + Parser::new("1").parse(), + Ok(EelFunction { + expressions: ExpressionBlock { + expressions: vec![Expression::NumberLiteral(NumberLiteral { value: 1.0 })] + } + }) + ); +} + +#[test] +fn can_parse_integer_2() { + assert_eq!( + Parser::new("2").parse(), + Ok(EelFunction { + expressions: ExpressionBlock { + expressions: vec![Expression::NumberLiteral(NumberLiteral { value: 2.0 })] + } + }) + ); +} diff --git a/compiler-rs/src/shim.rs b/compiler-rs/src/shim.rs new file mode 100644 index 0000000..d919898 --- /dev/null +++ b/compiler-rs/src/shim.rs @@ -0,0 +1,101 @@ +use crate::EelFunctionType; +use parity_wasm::elements::{FunctionType, ValueType}; + +// TODO: We could use https://docs.rs/strum_macros/0.20.1/strum_macros/index.html +#[derive(PartialEq, Eq, Hash)] +pub enum Shim { + Sin, + Pow, + Cos, + Tan, + Asin, + Acos, + Atan, + Atan2, + Log, + Log10, + Sigmoid, + Exp, +} + +impl Shim { + pub fn all() -> Vec { + vec![ + Shim::Sin, + Shim::Pow, + Shim::Cos, + Shim::Tan, + Shim::Asin, + Shim::Acos, + Shim::Atan, + Shim::Atan2, + Shim::Log, + Shim::Log10, + Shim::Sigmoid, + Shim::Exp, + ] + } + + pub fn get_type(&self) -> EelFunctionType { + FunctionType::new(self.get_args(), self.get_return()) + } + + pub fn get_args(&self) -> Vec { + vec![ValueType::F64; self.arity()] + } + + // All shims return a value + pub fn get_return(&self) -> Vec { + vec![ValueType::F64] + } + + pub fn arity(&self) -> usize { + match self { + Shim::Sin => 1, + Shim::Pow => 2, + Shim::Cos => 1, + Shim::Tan => 1, + Shim::Asin => 1, + Shim::Acos => 1, + Shim::Atan => 1, + Shim::Atan2 => 2, + Shim::Log => 1, + Shim::Log10 => 1, + Shim::Sigmoid => 2, + Shim::Exp => 1, + } + } + pub fn as_str(&self) -> &str { + match self { + Shim::Sin => "sin", + Shim::Pow => "pow", + Shim::Cos => "cos", + Shim::Tan => "tan", + Shim::Asin => "asin", + Shim::Acos => "acos", + Shim::Atan => "atan", + Shim::Atan2 => "atan2", + Shim::Log => "log", + Shim::Log10 => "log10", + Shim::Sigmoid => "sigmoid", + Shim::Exp => "exp", + } + } + pub fn from_str(name: &str) -> Option { + match name { + "sin" => Some(Shim::Sin), + "pow" => Some(Shim::Pow), + "cos" => Some(Shim::Cos), + "tan" => Some(Shim::Tan), + "asin" => Some(Shim::Asin), + "acos" => Some(Shim::Acos), + "atan" => Some(Shim::Atan), + "atan2" => Some(Shim::Atan2), + "log" => Some(Shim::Log), + "log10" => Some(Shim::Log10), + "sigmoid" => Some(Shim::Sigmoid), + "exp" => Some(Shim::Exp), + _ => None, + } + } +} diff --git a/compiler-rs/src/span.rs b/compiler-rs/src/span.rs new file mode 100644 index 0000000..f91e2d2 --- /dev/null +++ b/compiler-rs/src/span.rs @@ -0,0 +1,14 @@ +#[derive(Debug, PartialEq, Copy, Clone, Eq)] +pub struct Span { + pub start: u32, + pub end: u32, +} + +impl Span { + pub fn new(start: u32, end: u32) -> Self { + Span { start, end } + } + pub fn empty() -> Self { + Span { start: 0, end: 0 } + } +} diff --git a/compiler-rs/src/tokens.rs b/compiler-rs/src/tokens.rs new file mode 100644 index 0000000..fbcaf51 --- /dev/null +++ b/compiler-rs/src/tokens.rs @@ -0,0 +1,49 @@ +use super::span::Span; + +#[derive(Debug, PartialEq)] +pub enum TokenKind { + Int, + Plus, + Minus, + Bang, + Asterisk, + Slash, + Equal, + Identifier, + OpenParen, + CloseParen, + Comma, + Semi, + DoubleEqual, + Percent, + And, + AndAnd, + PipePipe, + Pipe, + Caret, + PlusEqual, + MinusEqual, + OpenAngel, + CloseAngel, + LTEqual, + GTEqual, + NotEqual, + TimesEqual, + DivEqual, + ModEqual, + Than, + EOF, + SOF, // Allows TokenKind to be non-optional in the parser +} + +#[derive(Debug, PartialEq)] +pub struct Token { + pub kind: TokenKind, + pub span: Span, +} + +impl Token { + pub fn new(kind: TokenKind, span: Span) -> Self { + Token { kind, span } + } +} diff --git a/compiler-rs/src/utils.rs b/compiler-rs/src/utils.rs new file mode 100644 index 0000000..165e944 --- /dev/null +++ b/compiler-rs/src/utils.rs @@ -0,0 +1,4 @@ +// TODO: There's got to be a better way. +pub fn f64_const(value: f64) -> u64 { + u64::from_le_bytes(value.to_le_bytes()) +} diff --git a/compiler-rs/tests/common/mod.rs b/compiler-rs/tests/common/mod.rs new file mode 100644 index 0000000..40026b2 --- /dev/null +++ b/compiler-rs/tests/common/mod.rs @@ -0,0 +1,238 @@ +extern crate eel_wasm; + +use std::{ + collections::{HashMap, HashSet}, + f64::EPSILON, +}; + +use eel_wasm::{compile, Shim}; +use wasmi::{nan_preserving_float::F64, ImportsBuilder}; +use wasmi::{ + Error as WasmiError, Externals, FuncInstance, FuncRef, GlobalDescriptor, GlobalInstance, + GlobalRef, ModuleImportResolver, RuntimeArgs, Signature, Trap, ValueType, +}; +use wasmi::{ModuleInstance, RuntimeValue}; + +pub struct GlobalPool { + g: GlobalRef, +} + +impl GlobalPool { + pub fn new() -> Self { + Self { + g: GlobalInstance::alloc(RuntimeValue::F64(0.0.into()), true), + } + } +} + +fn get_shim_index(shim: Shim) -> usize { + match shim { + Shim::Sin => 1, + Shim::Pow => 2, + Shim::Cos => 3, + Shim::Tan => 4, + Shim::Asin => 5, + Shim::Acos => 6, + Shim::Atan => 7, + Shim::Atan2 => 8, + Shim::Log => 9, + Shim::Log10 => 10, + Shim::Sigmoid => 11, + Shim::Exp => 12, + } +} + +fn get_shim_from_index(index: usize) -> Shim { + match index { + 1 => Shim::Sin, + 2 => Shim::Pow, + 3 => Shim::Cos, + 4 => Shim::Tan, + 5 => Shim::Asin, + 6 => Shim::Acos, + 7 => Shim::Atan, + 8 => Shim::Atan2, + 9 => Shim::Log, + 10 => Shim::Log10, + 11 => Shim::Sigmoid, + 12 => Shim::Exp, + _ => panic!("Could not find shim at index"), + } +} + +impl ModuleImportResolver for GlobalPool { + fn resolve_global( + &self, + field_name: &str, + _global_type: &GlobalDescriptor, + ) -> Result { + let global = match field_name { + "g" => self.g.clone(), + _ => GlobalInstance::alloc(RuntimeValue::F64(F64::from_float(0.0)), true), + }; + Ok(global) + } + fn resolve_func(&self, field_name: &str, signature: &Signature) -> Result { + let shim = Shim::from_str(field_name).ok_or(WasmiError::Instantiation(format!( + "Export {} not found", + field_name + )))?; + + if signature.params().len() != shim.arity() || !signature.return_type().is_some() { + return Err(WasmiError::Instantiation(format!( + "Export {} has a bad signature", + field_name + ))); + } + + let params = vec![ValueType::F64; shim.arity()]; + + Ok(FuncInstance::alloc_host( + Signature::new(params, Some(ValueType::F64)), + get_shim_index(shim), + )) + } +} + +impl Externals for GlobalPool { + fn invoke_index( + &mut self, + index: usize, + args: RuntimeArgs, + ) -> Result, Trap> { + match get_shim_from_index(index) { + Shim::Sin => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().sin(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Pow => { + let a: F64 = args.nth_checked(0)?; + let b: F64 = args.nth_checked(1)?; + + let result = a.to_float().powf(b.to_float()); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Cos => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().cos(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Tan => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().tan(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Asin => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().asin(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Acos => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().acos(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Atan => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().atan(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Atan2 => { + let a: F64 = args.nth_checked(0)?; + let b: F64 = args.nth_checked(1)?; + + let result = a.to_float().atan2(b.to_float()); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Log => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().log(std::f64::consts::E); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Log10 => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().log10(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Sigmoid => { + let a: F64 = args.nth_checked(0)?; + let b: F64 = args.nth_checked(1)?; + + let x = a.to_float(); + let y = b.to_float(); + + let t = 1.0 + (-x * y).exp(); + let result = if t.abs() > EPSILON { 1.0 / t } else { 0.0 }; + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + Shim::Exp => { + let a: F64 = args.nth_checked(0)?; + + let result = a.to_float().exp(); + + Ok(Some(RuntimeValue::F64(F64::from(result)))) + } + } + } +} + +pub fn eval_eel( + sources: Vec<(String, &str, String)>, + globals_map: HashMap>, + function_to_run: &str, +) -> Result { + // TODO: Avoid having to clone globals + let wasm_binary = compile(sources, globals_map.clone()) + .map_err(|err| format!("Compiler Error: {:?}", err))?; + + let module = wasmi::Module::from_buffer(&wasm_binary).map_err(|err| { + // TODO: Print out the wat? + println!("Wat: {}", wasmprinter::print_bytes(&wasm_binary).unwrap()); + format!("Error parsing binary Wasm: {}", err) + })?; + + let mut global_imports = GlobalPool::new(); + let mut imports = ImportsBuilder::default(); + + for (pool, _) in globals_map { + // TODO: Only make defined globals resolvable + imports.push_resolver(pool, &global_imports); + } + + imports.push_resolver("shims", &global_imports); + let instance = ModuleInstance::new(&module, &imports) + .map_err(|err| format!("Error instantiating Wasm module: {}", err))? + .assert_no_start(); + + // TODO: Instead of returning return value, return value of globals + match instance.invoke_export(function_to_run, &[], &mut global_imports) { + Ok(Some(_)) => Err("Did not expect to get return from eel function".to_string()), + Ok(None) => Ok(()), + Err(err) => Err(format!( + "Error invoking exported function {}: {}", + function_to_run, err + )), + }?; + let g = global_imports.g.get().try_into::().unwrap(); + Ok(g.into()) +} diff --git a/compiler-rs/tests/compatibility_test.rs b/compiler-rs/tests/compatibility_test.rs new file mode 100644 index 0000000..5365758 --- /dev/null +++ b/compiler-rs/tests/compatibility_test.rs @@ -0,0 +1,361 @@ +use std::collections::{HashMap, HashSet}; + +use common::eval_eel; + +mod common; + +#[test] +fn compatibility_tests() { + let test_cases: &[(&'static str, &'static str, f64)] = &[ + ("Expressions", "g = ((6- -7.0)+ 3.0);", 16.0), + ("Number", "g = 5;", 5.0), + ("Number with decimal", "g = 5.5;", 5.5), + ("Number with decimal and no leading whole", "g = .5;", 0.5), + ("Number with decimal and no trailing dec", "g = 5.;", 5.0), + ("Number with no digits", "g = .;", 0.0), + ("Optional final semi", "(g = 5; g = 10.0);", 10.0), + ("Unary negeation", "g = -10;", -10.0), + ("Unary plus", "g = +10;", 10.0), + ("Unary not true", "g = !10;", 0.0), + ("Unary not false", "g = !0;", 1.0), + ("Unary not 0.1", "g = !0.1;", 0.0), + ("Unary not < epsilon", "g = !0.000009;", 1.0), + ("Multiply", "g = 10 * 10;", 100.0), + ("Divide", "g = 10 / 10;", 1.0), + ("Mod", "g = 5 % 2;", 1.0), + ("Mod zero", "g = 5 % 0;", 0.0), + ("Bitwise and", "g = 3 & 5;", 1.0), + ("Bitwise or", "g = 3 | 5;", 7.0), + ("To the power", "g = 5 ^ 2;", 25.0), + ("Order of operations (+ and *)", "g = 1 + 1 * 10;", 11.0), + ("Order of operations (+ and /)", "g = 1 + 1 / 10;", 1.1), + ("Order of operations (unary - and +)", "g = -1 + 1;", 0.0), + ("Parens", "g = (1 + 1.0) * 10;", 20.0), + ("Absolute value negative", "g = abs(-10);", 10.0), + ("Absolute value positive", "g = abs(10);", 10.0), + ("Function used as expression", "g = 1 + abs(-10);", 11.0), + ("Min", "g = min(2, 10.0);", 2.0), + ("Min reversed", "g = min(10, 2.0);", 2.0), + ("Max", "g = max(2, 10.0);", 10.0), + ("Max reversed", "g = max(10, 2.0);", 10.0), + ("Sqrt", "g = sqrt(4);", 2.0), + ("Sqrt (negative)", "g = sqrt(-4);", 2.0), + ("Sqr", "g = sqr(10);", 100.0), + ("Int", "g = int(4.5);", 4.0), + ("Sin", "g = sin(10);", 10.0_f64.sin()), + ("Cos", "g = cos(10);", 10.0_f64.cos()), + ("Tan", "g = tan(10);", 10.0_f64.tan()), + ("Asin", "g = asin(0.5);", 0.5_f64.asin()), + ("Acos", "g = acos(0.5);", 0.5_f64.acos()), + ("Atan", "g = atan(0.5);", 0.5_f64.atan()), + ("Atan2", "g = atan2(1, 1.0);", 1_f64.atan2(1.0)), + ("Assign to globals", "g = 10;", 10.0), + // ("Read globals", "g = x;", 10.0), + ("Multiple statements", "g = 10; g = 20;", 20.0), + ("Multiple statements expression", "(g = 10; g = 20;);", 20.0), + ( + "Multiple statements expression implcit return", + "g = (0; 20 + 5;);", + 25.0, + ), + ("if", "g = if(0, 20, 10.0);", 10.0), + ("if", "g = if(0, 20, 10.0);", 10.0), + ( + "if does short-circit (consiquent)", + "if(0, (g = 10;), 10.0);", + 0.0, + ), + ( + "if does short-circit (alternate)", + "if(1, (10), (g = 10;));", + 0.0, + ), + ("above (true)", "g = above(10, 4.0);", 1.0), + ("above (false)", "g = above(4, 10.0);", 0.0), + ("below (true)", "g = below(4, 10.0);", 1.0), + ("below (false)", "g = below(10, 4.0);", 0.0), + ("Line comments", "g = 10; // g = 20;", 10.0), + ("Line comments (\\\\)", "g = 10; \\\\ g = 20;", 10.0), + ("Equal (false)", "g = equal(10, 5.0);", 0.0), + ("Equal (true)", "g = equal(10, 10.0);", 1.0), + ("Log", "g = log(10);", 10_f64.log(std::f64::consts::E)), + ("Log10", "g = log10(10);", 10_f64.log10()), + ("Sign (10)", "g = sign(10);", 1.0), + ("Sign (-10)", "g = sign(-10);", -1.0), + ("Sign (0)", "g = sign(0);", 0.0), + ("Sign (-0)", "g = sign(-0);", 0.0), + ("Local variables", "a = 10; g = a * a;", 100.0), + ( + "Local variable assignment (implicit return)", + "g = a = 10;", + 10.0, + ), + ("Bor (true, false)", "g = bor(10, 0.0);", 1.0), + ("Bor (false, true)", "g = bor(0, 2.0);", 1.0), + ("Bor (true, true)", "g = bor(1, 7.0);", 1.0), + ("Bor (false, false)", "g = bor(0, 0.0);", 0.0), + ("Bor does not shortcircut", "bor(1, g = 10.0);", 10.0), + ("Bor respects epsilon", "g = bor(0.000009, 0.000009);", 0.0), + ("Band (true, false)", "g = band(10, 0.0);", 0.0), + ("Band (false, true)", "g = band(0, 2.0);", 0.0), + ("Band (true, true)", "g = band(1, 7.0);", 1.0), + ("Band (false, false)", "g = band(0, 0.0);", 0.0), + ("Band does not shortcircut", "band(0, g = 10.0);", 10.0), + ( + "Band respects epsilon", + "g = band(0.000009, 0.000009);", + 0.0, + ), + ("Bnot (true)", "g = bnot(10);", 0.0), + ("Bnot (false)", "g = bnot(0);", 1.0), + ("Bnot 0.1", "g = bnot(0.1);", 0.0), + ("Bnot < epsilon", "g = bnot(0.000009);", 1.0), + ("Plus equals", "g = 5; g += 5;", 10.0), + ("Plus equals (local var)", "a = 5; a += 5; g = a;", 10.0), + ("Plus equals (megabuf)", "g = megabuf(0) += 5;", 5.0), + ("Minus equals", "g = 5; g -= 4;", 1.0), + ("Minus equals (local var)", "a = 5; a -= 4; g = a;", 1.0), + ("Minus equals (megabuf)", "g = megabuf(0) -= 5;", -5.0), + ("Times equals", "g = 5; g *= 4;", 20.0), + ("Times equals (local var)", "a = 5; a *= 4; g = a;", 20.0), + ( + "Times equals (megabuf)", + "g = (megabuf(0) = 9; megabuf(0) *= 2.0);", + 18.0, + ), + ("Divide equals", "g = 5; g /= 2;", 2.5), + ("Divide equals (local var)", "a = 5; a /= 2; g = a;", 2.5), + ( + "Divide equals (megabuf)", + "g = (megabuf(0) = 8; megabuf(0) /= 2.0);", + 4.0, + ), + ("Mod equals", "g = 5; g %= 2;", 1.0), + ("Mod equals (local var)", "a = 5; a %= 2; g = a;", 1.0), + ( + "Mod equals (megabuf)", + "g = (megabuf(0) = 5; megabuf(0) %= 2.0);", + 1.0, + ), + ( + "Statement block as argument", + "g = int(g = 5; g + 10.5;);", + 15.0, + ), + ("Logical and (both true)", "g = 10 && 2;", 1.0), + ( + "Logical and does not run the left twice", + "(g = g + 1; 0;) && 10;", + 1.0, + ), + ("Logical and (first value false)", "g = 0 && 2;", 0.0), + ("Logical and (second value false)", "g = 2 && 0;", 0.0), + ("Logical or (both true)", "g = 10 || 2;", 1.0), + ("Logical or (first value false)", "g = 0 || 2;", 1.0), + ("Logical and shortcircuts", "0 && g = 10;", 0.0), + ("Logical or shortcircuts", "1 || g = 10;", 0.0), + ("Exec2", "g = exec2(x = 5, x * 3.0);", 15.0), + ("Exec3", "g = exec3(x = 5, x = x * 3, x + 1.0);", 16.0), + ("While", "while(exec2(g = g + 1, g - 10.0));", 10.0), + ("Loop", "loop(10, g = g + 1.0);", 10.0), + ("Loop fractional times", "loop(1.5, g = g + 1.0);", 1.0), + ("Loop zero times", "loop(0, g = g + 1.0);", 0.0), + ("Loop negative times", "loop(-2, g = g + 1.0);", 0.0), + ( + "Loop negative fractional times", + "loop(-0.2, g = g + 1.0);", + 0.0, + ), + ("Equality (true)", "g = 1 == 1;", 1.0), + ("Equality epsilon", "g = 0 == 0.000009;", 1.0), + ("!Equality (true)", "g = 1 != 0;", 1.0), + ("!Equality (false)", "g = 1 != 1;", 0.0), + ("!Equality epsilon", "g = 0 != 0.000009;", 0.0), + ("Equality (false)", "g = 1 == 0;", 0.0), + ("Less than (true)", "g = 1 < 2;", 1.0), + ("Less than (false)", "g = 2 < 1;", 0.0), + ("Greater than (true)", "g = 2 > 1;", 1.0), + ("Greater than (false)", "g = 1 > 2;", 0.0), + ("Less than or equal (true)", "g = 1 <= 2;", 1.0), + ("Less than or equal (false)", "g = 2 <= 1;", 0.0), + ("Greater than or equal (true)", "g = 2 >= 1;", 1.0), + ("Greater than or equal (false)", "g = 1 >= 2;", 0.0), + ("Script without trailing semi", "g = 1", 1.0), + ("Megabuf access", "g = megabuf(1);", 0.0), + ( + "Max index megabuf", + "megabuf(8388607) = 10; g = megabuf(8388607);", + 10.0, + ), + ( + "Max index + 1 megabuf", + "megabuf(8388608) = 10; g = megabuf(8388608);", + 0.0, + ), + ( + "Max index gmegabuf", + "gmegabuf(8388607) = 10; g = gmegabuf(8388607);", + 10.0, + ), + ( + "Max index+1 gmegabuf", + "gmegabuf(8388608) = 10; g = gmegabuf(8388608);", + 0.0, + ), + ( + "Megabuf assignment", + "megabuf(1) = 10; g = megabuf(1);", + 10.0, + ), + ( + "Megabuf assignment (idx 100.0)", + "megabuf(100) = 10; g = megabuf(100);", + 10.0, + ), + ("Megabuf (float)", "megabuf(0) = 1.2; g = megabuf(0);", 1.2), + ("Gmegabuf", "gmegabuf(0) = 1.2; g = gmegabuf(0);", 1.2), + ( + "Megabuf != Gmegabuf", + "gmegabuf(0) = 1.2; g = megabuf(0);", + 0.0, + ), + ( + "Gmegabuf != Megabuf", + "megabuf(0) = 1.2; g = gmegabuf(0);", + 0.0, + ), + ("Case insensitive vars", "G = 10;", 10.0), + ("Case insensitive funcs", "g = InT(10);", 10.0), + ("Consecutive semis", "g = 10;;; ;g = 20;;", 20.0), + ("Equality (< epsilon)", "g = 0.000009 == 0;", 1.0), + ("Equality (< -epsilon)", "g = -0.000009 == 0;", 1.0), + ("Variables don't collide", "g = 1; not_g = 2;", 1.0), + ("Simple block comment", "g = 1; /* g = 10 */", 1.0), + ("Block comment", "g = 1; /* g = 10 */ g = g * 2;", 2.0), + ("Sigmoid 1, 2", "g = sigmoid(1, 2.0);", 0.8807970779778823), + ("Sigmoid 2, 1", "g = sigmoid(2, 1.0);", 0.8807970779778823), + ("Sigmoid 0, 0", "g = sigmoid(0, 0.0);", 0.5), + ("Sigmoid 10, 10", "g = sigmoid(10, 10.0);", 1.0), + ("Exp", "g = exp(10);", 10_f64.exp()), + ("Floor", "g = floor(10.9);", 10.0), + ("Floor", "g = floor(-10.9);", -11.0), + ("Ceil", "g = ceil(9.1);", 10.0), + ("Ceil", "g = ceil(-9.9);", -9.0), + ("Assign", "assign(g, 10.0);", 10.0), + ("Assign return value", "g = assign(x, 10.0);", 10.0), + ( + "EPSILON buffer indexes", + "megabuf(9.99999) = 10; g = megabuf(10)", + 10.0, + ), + ( + "+EPSILON & rounding -#s toward 0", + "megabuf(-1) = 10; g = megabuf(0)", + 10.0, + ), + ("Negative buffer index read as 0", "g = megabuf(-2);", 0.0), + ("Negative buffer index", "g = (megabuf(-2) = 20.0);", 0.0), + ( + "Negative buffer index gmegabuf", + "g = (gmegabuf(-2) = 20.0);", + 0.0, + ), + ( + "Negative buf index execs right hand side", + "megabuf(-2) = (g = 10.0);", + 10.0, + ), + ("Negative buf index +=", "g = megabuf(-2) += 10;", 10.0), + ("Negative buf index -=", "g = megabuf(-2) -= 10;", -10.0), + ("Negative buf index *=", "g = megabuf(-2) *= 10;", 0.0), + ("Negative buf index /=", "g = megabuf(-2) /= 10;", 0.0), + ("Negative buf index %=", "g = megabuf(-2) %= 10;", 0.0), + ( + "Buff += mutates", + "megabuf(100) += 10; g = megabuf(100)", + 10.0, + ), + ( + "Buffers don't collide", + "megabuf(100) = 10; g = gmegabuf(100)", + 0.0, + ), + ( + "gmegabuf does not write megabuf", + "i = 100; loop(10000,gmegabuf(i) = 10; i += 1.0); g = megabuf(100)", + 0.0, + ), + ( + "megabuf does not write gmegabuf", + "i = 100; loop(10000,megabuf(i) = 10; i += 1.0); g = gmegabuf(100)", + 0.0, + ), + ( + "Adjacent buf indicies don't collide", + "megabuf(99) = 10; megabuf(100) = 1; g = megabuf(99)", + 10.0, + ), + ("Exponentiation associativity", "g = 2 ^ 2 ^ 4", 256.0), + ( + "^ has lower precedence than * (left)", + "g = 2 ^ 2 * 4", + 16.0, + ), + ( + "^ has lower precedence than * (right)", + "g = 2 * 2 ^ 4", + 32.0, + ), + ( + "% has lower precedence than * (right)", + "g = 2 * 5 % 2", + 2.0, + ), + ("% has lower precedence than * (left)", "g = 2 % 5 * 2", 4.0), + ( + "% and ^ have the same precedence (% first)", + "g = 2 % 5 ^ 2", + 4.0, + ), + ( + "% and ^ have the same precedence (^ first)", + "g = 2 ^ 5 % 2", + 0.0, + ), + ("Loop limit", "g = 0; while(g = g + 1.0)", 1048576.0), + ("Divide by zero", "g = 100 / 0", 0.0), + ( + "Divide by less than epsilon", + "g = 100 / 0.000001", + 100000000.0, + ), + ]; + + for (name, code, expected) in test_cases { + let mut globals = HashMap::default(); + let mut pool_globals = HashSet::new(); + pool_globals.insert("g".to_string()); + // TODO: We should set x = 10 + globals.insert("pool".to_string(), pool_globals); + match eval_eel( + vec![("test".to_string(), code, "pool".to_string())], + globals, + "test", + ) { + Ok(actual) => { + if &actual != expected { + panic!(format!( + "Bad result for {}. Expected {}, but got {}.", + name, expected, actual + )); + } + } + Err(err) => { + panic!(format!( + "Didn't expect \"{}\" to fail. Failed with {:?}", + name, err + )); + } + } + } +} diff --git a/compiler-rs/tests/fixtures/ast/local_variables.eel b/compiler-rs/tests/fixtures/ast/local_variables.eel new file mode 100644 index 0000000..b137939 --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/local_variables.eel @@ -0,0 +1 @@ +a = 10; g = a * a; \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/ast/local_variables.snapshot b/compiler-rs/tests/fixtures/ast/local_variables.snapshot new file mode 100644 index 0000000..b6dfc4f --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/local_variables.snapshot @@ -0,0 +1,64 @@ +a = 10; g = a * a; +======================================================================== +EelFunction { + expressions: ExpressionBlock { + expressions: [ + Assignment( + Assignment { + left: Identifier( + Identifier { + name: "a", + span: Span { + start: 0, + end: 1, + }, + }, + ), + operator: Equal, + right: NumberLiteral( + NumberLiteral { + value: 10.0, + }, + ), + }, + ), + Assignment( + Assignment { + left: Identifier( + Identifier { + name: "g", + span: Span { + start: 8, + end: 9, + }, + }, + ), + operator: Equal, + right: BinaryExpression( + BinaryExpression { + left: Identifier( + Identifier { + name: "a", + span: Span { + start: 12, + end: 13, + }, + }, + ), + right: Identifier( + Identifier { + name: "a", + span: Span { + start: 16, + end: 17, + }, + }, + ), + op: Multiply, + }, + ), + }, + ), + ], + }, +} diff --git a/compiler-rs/tests/fixtures/ast/one_plus_one.eel b/compiler-rs/tests/fixtures/ast/one_plus_one.eel new file mode 100644 index 0000000..07d91dc --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/one_plus_one.eel @@ -0,0 +1 @@ +1+1 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/ast/one_plus_one.snapshot b/compiler-rs/tests/fixtures/ast/one_plus_one.snapshot new file mode 100644 index 0000000..cd0821f --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/one_plus_one.snapshot @@ -0,0 +1,23 @@ +1+1 +======================================================================== +EelFunction { + expressions: ExpressionBlock { + expressions: [ + BinaryExpression( + BinaryExpression { + left: NumberLiteral( + NumberLiteral { + value: 1.0, + }, + ), + right: NumberLiteral( + NumberLiteral { + value: 1.0, + }, + ), + op: Add, + }, + ), + ], + }, +} diff --git a/compiler-rs/tests/fixtures/ast/order_of_operations_unary.eel b/compiler-rs/tests/fixtures/ast/order_of_operations_unary.eel new file mode 100644 index 0000000..a824a89 --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/order_of_operations_unary.eel @@ -0,0 +1 @@ +g = -1 + 1; \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/ast/order_of_operations_unary.snapshot b/compiler-rs/tests/fixtures/ast/order_of_operations_unary.snapshot new file mode 100644 index 0000000..7280046 --- /dev/null +++ b/compiler-rs/tests/fixtures/ast/order_of_operations_unary.snapshot @@ -0,0 +1,42 @@ +g = -1 + 1; +======================================================================== +EelFunction { + expressions: ExpressionBlock { + expressions: [ + Assignment( + Assignment { + left: Identifier( + Identifier { + name: "g", + span: Span { + start: 0, + end: 1, + }, + }, + ), + operator: Equal, + right: BinaryExpression( + BinaryExpression { + left: UnaryExpression( + UnaryExpression { + right: NumberLiteral( + NumberLiteral { + value: 1.0, + }, + ), + op: Minus, + }, + ), + right: NumberLiteral( + NumberLiteral { + value: 1.0, + }, + ), + op: Add, + }, + ), + }, + ), + ], + }, +} diff --git a/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.eel b/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.eel new file mode 100644 index 0000000..d928b17 --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.eel @@ -0,0 +1 @@ +1\1 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.snapshot b/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.snapshot new file mode 100644 index 0000000..febbfe1 --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/one_backslash_one.invalid.snapshot @@ -0,0 +1,3 @@ +1\1 +======================================================================== +Parse Error: Unexpected character '1' following \. diff --git a/compiler-rs/tests/fixtures/tokens/one_plus_one.eel b/compiler-rs/tests/fixtures/tokens/one_plus_one.eel new file mode 100644 index 0000000..07d91dc --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/one_plus_one.eel @@ -0,0 +1 @@ +1+1 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/tokens/one_plus_one.snapshot b/compiler-rs/tests/fixtures/tokens/one_plus_one.snapshot new file mode 100644 index 0000000..f80db65 --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/one_plus_one.snapshot @@ -0,0 +1,7 @@ +1+1 +======================================================================== +[ + Int, + Plus, + Int, +] diff --git a/compiler-rs/tests/fixtures/tokens/whitespace.eel b/compiler-rs/tests/fixtures/tokens/whitespace.eel new file mode 100644 index 0000000..567126e --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/whitespace.eel @@ -0,0 +1,5 @@ +1 + 2 +- 100 + + +- 1 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/tokens/whitespace.snapshot b/compiler-rs/tests/fixtures/tokens/whitespace.snapshot new file mode 100644 index 0000000..b399ad2 --- /dev/null +++ b/compiler-rs/tests/fixtures/tokens/whitespace.snapshot @@ -0,0 +1,15 @@ +1 + 2 +- 100 + + +- 1 +======================================================================== +[ + Int, + Plus, + Int, + Minus, + Int, + Minus, + Int, +] diff --git a/compiler-rs/tests/fixtures/wat/div.eel b/compiler-rs/tests/fixtures/wat/div.eel new file mode 100644 index 0000000..f2e4433 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/div.eel @@ -0,0 +1 @@ +g = 100 / 0 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/div.snapshot b/compiler-rs/tests/fixtures/wat/div.snapshot new file mode 100644 index 0000000..5223f01 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/div.snapshot @@ -0,0 +1,40 @@ +g = 100 / 0 +======================================================================== +(module + (type (;0;) (func (param f64) (result f64))) + (type (;1;) (func (param f64 f64) (result f64))) + (type (;2;) (func)) + (import "shims" "sin" (func (;0;) (type 0))) + (import "shims" "pow" (func (;1;) (type 1))) + (import "shims" "cos" (func (;2;) (type 0))) + (import "shims" "tan" (func (;3;) (type 0))) + (import "shims" "asin" (func (;4;) (type 0))) + (import "shims" "acos" (func (;5;) (type 0))) + (import "shims" "atan" (func (;6;) (type 0))) + (import "shims" "atan2" (func (;7;) (type 1))) + (import "shims" "log" (func (;8;) (type 0))) + (import "shims" "log10" (func (;9;) (type 0))) + (import "shims" "sigmoid" (func (;10;) (type 1))) + (import "shims" "exp" (func (;11;) (type 0))) + (func (;12;) (type 2) + f64.const 0x1.9p+6 (;=100;) + f64.const 0x0p+0 (;=0;) + call 13 + global.set 0 + global.get 0 + drop) + (func (;13;) (type 1) (param f64 f64) (result f64) + (local i32) + local.get 1 + f64.const 0x0p+0 (;=0;) + f64.ne + if (result f64) ;; label = @1 + local.get 0 + local.get 1 + f64.div + else + f64.const 0x0p+0 (;=0;) + end) + (memory (;0;) 2048 2048) + (global (;0;) (mut f64) (f64.const 0x0p+0 (;=0;))) + (export "test" (func 12))) diff --git a/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.eel b/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.eel new file mode 100644 index 0000000..63b166a --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.eel @@ -0,0 +1 @@ +1+ \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.snapshot b/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.snapshot new file mode 100644 index 0000000..48de112 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/one_plus_nothing.invalid.snapshot @@ -0,0 +1,3 @@ +1+ +======================================================================== +Expected Int or Identifier but got EOF diff --git a/compiler-rs/tests/fixtures/wat/one_plus_one.eel b/compiler-rs/tests/fixtures/wat/one_plus_one.eel new file mode 100644 index 0000000..07d91dc --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/one_plus_one.eel @@ -0,0 +1 @@ +1+1 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/one_plus_one.snapshot b/compiler-rs/tests/fixtures/wat/one_plus_one.snapshot new file mode 100644 index 0000000..00b9083 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/one_plus_one.snapshot @@ -0,0 +1,25 @@ +1+1 +======================================================================== +(module + (type (;0;) (func (param f64) (result f64))) + (type (;1;) (func (param f64 f64) (result f64))) + (type (;2;) (func)) + (import "shims" "sin" (func (;0;) (type 0))) + (import "shims" "pow" (func (;1;) (type 1))) + (import "shims" "cos" (func (;2;) (type 0))) + (import "shims" "tan" (func (;3;) (type 0))) + (import "shims" "asin" (func (;4;) (type 0))) + (import "shims" "acos" (func (;5;) (type 0))) + (import "shims" "atan" (func (;6;) (type 0))) + (import "shims" "atan2" (func (;7;) (type 1))) + (import "shims" "log" (func (;8;) (type 0))) + (import "shims" "log10" (func (;9;) (type 0))) + (import "shims" "sigmoid" (func (;10;) (type 1))) + (import "shims" "exp" (func (;11;) (type 0))) + (func (;12;) (type 2) + f64.const 0x1p+0 (;=1;) + f64.const 0x1p+0 (;=1;) + f64.add + drop) + (memory (;0;) 2048 2048) + (export "test" (func 12))) diff --git a/compiler-rs/tests/fixtures/wat/pow.eel b/compiler-rs/tests/fixtures/wat/pow.eel new file mode 100644 index 0000000..deb9412 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/pow.eel @@ -0,0 +1 @@ +g = 5 ^ 2; \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/pow.snapshot b/compiler-rs/tests/fixtures/wat/pow.snapshot new file mode 100644 index 0000000..4170373 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/pow.snapshot @@ -0,0 +1,28 @@ +g = 5 ^ 2; +======================================================================== +(module + (type (;0;) (func (param f64) (result f64))) + (type (;1;) (func (param f64 f64) (result f64))) + (type (;2;) (func)) + (import "shims" "sin" (func (;0;) (type 0))) + (import "shims" "pow" (func (;1;) (type 1))) + (import "shims" "cos" (func (;2;) (type 0))) + (import "shims" "tan" (func (;3;) (type 0))) + (import "shims" "asin" (func (;4;) (type 0))) + (import "shims" "acos" (func (;5;) (type 0))) + (import "shims" "atan" (func (;6;) (type 0))) + (import "shims" "atan2" (func (;7;) (type 1))) + (import "shims" "log" (func (;8;) (type 0))) + (import "shims" "log10" (func (;9;) (type 0))) + (import "shims" "sigmoid" (func (;10;) (type 1))) + (import "shims" "exp" (func (;11;) (type 0))) + (func (;12;) (type 2) + f64.const 0x1.4p+2 (;=5;) + f64.const 0x1p+1 (;=2;) + call 1 + global.set 0 + global.get 0 + drop) + (memory (;0;) 2048 2048) + (global (;0;) (mut f64) (f64.const 0x0p+0 (;=0;))) + (export "test" (func 12))) diff --git a/compiler-rs/tests/fixtures/wat/reg.eel b/compiler-rs/tests/fixtures/wat/reg.eel new file mode 100644 index 0000000..c707df0 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/reg.eel @@ -0,0 +1 @@ +reg00=10 \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/reg.snapshot b/compiler-rs/tests/fixtures/wat/reg.snapshot new file mode 100644 index 0000000..7cd8f98 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/reg.snapshot @@ -0,0 +1,26 @@ +reg00=10 +======================================================================== +(module + (type (;0;) (func (param f64) (result f64))) + (type (;1;) (func (param f64 f64) (result f64))) + (type (;2;) (func)) + (import "shims" "sin" (func (;0;) (type 0))) + (import "shims" "pow" (func (;1;) (type 1))) + (import "shims" "cos" (func (;2;) (type 0))) + (import "shims" "tan" (func (;3;) (type 0))) + (import "shims" "asin" (func (;4;) (type 0))) + (import "shims" "acos" (func (;5;) (type 0))) + (import "shims" "atan" (func (;6;) (type 0))) + (import "shims" "atan2" (func (;7;) (type 1))) + (import "shims" "log" (func (;8;) (type 0))) + (import "shims" "log10" (func (;9;) (type 0))) + (import "shims" "sigmoid" (func (;10;) (type 1))) + (import "shims" "exp" (func (;11;) (type 0))) + (func (;12;) (type 2) + f64.const 0x1.4p+3 (;=10;) + global.set 0 + global.get 0 + drop) + (memory (;0;) 2048 2048) + (global (;0;) (mut f64) (f64.const 0x0p+0 (;=0;))) + (export "test" (func 12))) diff --git a/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.eel b/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.eel new file mode 100644 index 0000000..052b158 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.eel @@ -0,0 +1 @@ +sin(100,200) \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.snapshot b/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.snapshot new file mode 100644 index 0000000..f5696af --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/shims_wrong_args.invalid.snapshot @@ -0,0 +1,3 @@ +sin(100,200) +======================================================================== +Incorrect argument count for function `sin`. Expected 1 but got 2. diff --git a/compiler-rs/tests/fixtures/wat/sin.eel b/compiler-rs/tests/fixtures/wat/sin.eel new file mode 100644 index 0000000..5204d29 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/sin.eel @@ -0,0 +1 @@ +sin(100) \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/sin.snapshot b/compiler-rs/tests/fixtures/wat/sin.snapshot new file mode 100644 index 0000000..8ee87e2 --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/sin.snapshot @@ -0,0 +1,24 @@ +sin(100) +======================================================================== +(module + (type (;0;) (func (param f64) (result f64))) + (type (;1;) (func (param f64 f64) (result f64))) + (type (;2;) (func)) + (import "shims" "sin" (func (;0;) (type 0))) + (import "shims" "pow" (func (;1;) (type 1))) + (import "shims" "cos" (func (;2;) (type 0))) + (import "shims" "tan" (func (;3;) (type 0))) + (import "shims" "asin" (func (;4;) (type 0))) + (import "shims" "acos" (func (;5;) (type 0))) + (import "shims" "atan" (func (;6;) (type 0))) + (import "shims" "atan2" (func (;7;) (type 1))) + (import "shims" "log" (func (;8;) (type 0))) + (import "shims" "log10" (func (;9;) (type 0))) + (import "shims" "sigmoid" (func (;10;) (type 1))) + (import "shims" "exp" (func (;11;) (type 0))) + (func (;12;) (type 2) + f64.const 0x1.9p+6 (;=100;) + call 0 + drop) + (memory (;0;) 2048 2048) + (export "test" (func 12))) diff --git a/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.eel b/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.eel new file mode 100644 index 0000000..b83a49a --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.eel @@ -0,0 +1 @@ +if(1,2) \ No newline at end of file diff --git a/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.snapshot b/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.snapshot new file mode 100644 index 0000000..cb188fd --- /dev/null +++ b/compiler-rs/tests/fixtures/wat/wrong_arity_if.invalid.snapshot @@ -0,0 +1,3 @@ +if(1,2) +======================================================================== +Incorrect argument count for function `if`. Expected 3 but got 2. diff --git a/compiler-rs/tests/integration_test.rs b/compiler-rs/tests/integration_test.rs new file mode 100644 index 0000000..0c6351a --- /dev/null +++ b/compiler-rs/tests/integration_test.rs @@ -0,0 +1,77 @@ +extern crate eel_wasm; +mod common; + +use std::collections::{HashMap, HashSet}; + +use common::{eval_eel, GlobalPool}; +use eel_wasm::compile; +use wasmi::{ImportsBuilder, ModuleInstance}; + +fn test_run(program: &str, expected_output: f64) { + let mut globals = HashMap::default(); + let mut pool_globals = HashSet::new(); + pool_globals.insert("g".to_string()); + globals.insert("pool".to_string(), pool_globals); + + let result = eval_eel( + vec![("test".to_string(), program, "pool".to_string())], + globals, + "test", + ); + assert_eq!(result, Ok(expected_output)); +} + +#[test] +fn execute_one() { + test_run("g=1", 1.0); + test_run("g=1+1", 2.0); + test_run("g=1-1", 0.0); + test_run("g=2*2", 4.0); + test_run("g=2/2", 1.0); + + test_run("g=1+1*2", 3.0); +} + +#[test] +fn with_global() { + test_run("g=1", 1.0); +} + +#[test] +fn with_shims() { + test_run("g=sin(10)", 10.0_f64.sin()); +} + +#[test] +fn multiple_functions() { + let wasm_binary = compile( + vec![ + ("one".to_string(), "1", "pool".to_string()), + ("two".to_string(), "2", "pool".to_string()), + ], + HashMap::default(), + ) + .expect("Expect to compile"); + // TODO: This will fail becuase wasmi 0.8.0 depends upon wasmi-validaiton + // 0.3.0 which does not include https://github.com/paritytech/wasmi/pull/228 + // which allows mutable globals. + // 0.3.1 has the PR, but wasmi has not shipped a new version that includes it. + // parity-wasm already depends upon 0.3.1 (I _think_) + let module = wasmi::Module::from_buffer(&wasm_binary).expect("No validation errors"); + let mut global_imports = GlobalPool::new(); + let mut imports = ImportsBuilder::default(); + imports.push_resolver("pool", &global_imports); + imports.push_resolver("shims", &global_imports); + + let instance = ModuleInstance::new(&module, &imports) + .expect("failed to instantiate wasm module") + .assert_no_start(); + + instance + .invoke_export("one", &[], &mut global_imports) + .expect("failed to execute export"); + + instance + .invoke_export("two", &[], &mut global_imports) + .expect("failed to execute export"); +} diff --git a/compiler-rs/tests/snapshots_test.rs b/compiler-rs/tests/snapshots_test.rs new file mode 100644 index 0000000..2a769ff --- /dev/null +++ b/compiler-rs/tests/snapshots_test.rs @@ -0,0 +1,123 @@ +extern crate eel_wasm; + +use std::fs; +use std::io; +use std::path::PathBuf; +use std::{collections::HashMap, env}; + +use eel_wasm::{compile, Lexer, TokenKind}; +use eel_wasm::{parse, Token}; + +fn get_fixture_dir_path(dir: &str) -> io::Result { + let mut fixture_dir = env::current_dir()?; + fixture_dir.push(dir); + Ok(fixture_dir) +} + +#[test] +fn run_snapshots() -> io::Result<()> { + run_snapshots_impl("tests/fixtures/wat", compiler_transform)?; + run_snapshots_impl("tests/fixtures/ast", ast_transform)?; + run_snapshots_impl("tests/fixtures/tokens", lexer_transform)?; + Ok(()) +} + +fn run_snapshots_impl(dir: &str, transform: F) -> io::Result<()> +where + F: Fn(&str) -> (String, bool), +{ + let line = "========================================================================"; + for entry in fs::read_dir(get_fixture_dir_path(dir)?)? { + let entry = entry?; + let path = &entry.path(); + if let Some(ext) = path.extension() { + if ext == "snapshot" { + // TODO: We could assert that this snapshot file has a matching + // source file and clean up any extranious files. + continue; + } + } + let source_path = path.file_name().unwrap().to_string_lossy(); + let expected_invalid = source_path.ends_with(".invalid.eel"); + let snapshot_file_path = path.with_extension("snapshot"); + + let source = fs::read_to_string(path)?; + + let (output_str, actual_invalid) = transform(&source); + + let snapshot = format!("{}\n{}\n{}\n", source, line, output_str); + + if !snapshot_file_path.exists() || env::var("UPDATE_SNAPSHOTS").is_ok() { + fs::write(snapshot_file_path, snapshot)?; + } else { + let actual_snapshot = fs::read_to_string(snapshot_file_path)?; + // TODO: We could improve the diff output + // TODO: We could inform the user that they can set the UPDATE_SNAPSHOTS environment variable to update. + assert_eq!(snapshot, actual_snapshot, "Expected snapshot for {} to match, but it did not.\nRerun with `UPDATE_SNAPSHOTS=1 to update.`", source_path); + } + + let actual_str = if actual_invalid { "invalid" } else { "valid" }; + let expected_str = if expected_invalid { "invalid" } else { "valid" }; + + assert_eq!( + actual_invalid, expected_invalid, + "Expected file \"{}\" to be {} but it was {}", + source_path, expected_str, actual_str, + ); + } + + Ok(()) +} + +fn compiler_transform(src: &str) -> (String, bool) { + let output = compile( + vec![("test".to_string(), src, "pool".to_string())], + HashMap::default(), + ); + + let actual_invaid = output.is_err(); + + let output_str: String = match output { + Ok(binary) => wasmprinter::print_bytes(&binary).unwrap(), + Err(err) => err.pretty_print(src), + }; + (output_str, actual_invaid) +} + +fn ast_transform(src: &str) -> (String, bool) { + let output = parse(src); + + let actual_invaid = output.is_err(); + + let output_str: String = match output { + Ok(ast) => format!("{:#?}", ast), + Err(err) => err.pretty_print(src), + }; + (output_str, actual_invaid) +} + +fn lexer_transform(src: &str) -> (String, bool) { + let mut lexer = Lexer::new(src); + let mut tokens = vec![]; + let mut error = None; + loop { + match lexer.next_token() { + Err(err) => { + error = Some(err); + break; + } + Ok(Token { + kind: TokenKind::EOF, + .. + }) => break, + Ok(Token { kind, .. }) => tokens.push(kind), + } + } + + let actual_invalid = error.is_some(); + let output_str = match error { + Some(err) => err.pretty_print(src), + None => format!("{:#?}", tokens), + }; + (output_str, actual_invalid) +} diff --git a/compiler-rs/tests/wasm_test.rs b/compiler-rs/tests/wasm_test.rs new file mode 100644 index 0000000..36afef8 --- /dev/null +++ b/compiler-rs/tests/wasm_test.rs @@ -0,0 +1,41 @@ +extern crate wabt; +extern crate wasmi; + +use wasmi::ImportsBuilder; +use wasmi::{ModuleInstance, NopExternals, RuntimeValue}; + +#[test] +fn wasm_test() { + // Parse WAT (WebAssembly Text format) into wasm bytecode. + let wasm_binary: Vec = wabt::wat2wasm( + r#" + (module + (func (export "test") (result f64) + f64.const 1 + f64.const 1 + f64.add + ) + ) + "#, + ) + .expect("failed to parse wat"); + + // println!("{:?}", wasm_binary); + // Load wasm binary and prepare it for instantiation. + let module = wasmi::Module::from_buffer(&wasm_binary).expect("failed to load wasm"); + let imports = ImportsBuilder::default(); + // Instantiate a module with empty imports and + // assert that there is no `start` function. + let instance = ModuleInstance::new(&module, &imports) + .expect("failed to instantiate wasm module") + .assert_no_start(); + + // Finally, invoke the exported function "test" with no parameters + // and empty external function executor. + assert_eq!( + instance + .invoke_export("test", &[], &mut NopExternals,) + .expect("failed to execute export"), + Some(RuntimeValue::F64(2.0.into())), + ); +} diff --git a/packages/compiler/src/__tests__/encoding.test.ts b/packages/compiler/src/__tests__/encoding.test.ts new file mode 100644 index 0000000..c36c72a --- /dev/null +++ b/packages/compiler/src/__tests__/encoding.test.ts @@ -0,0 +1,4 @@ +import { encodef64 } from "../encoding"; +test("float", () => { + expect(encodef64(1)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 240, 63])); +});