From cdc6449f5308d4362fd0d24078ac8f44c6fd2e31 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Aug 2024 10:16:51 -0400 Subject: [PATCH] Initial commit --- .gitignore | 6 + Cargo.lock | 48 ++++++++ Cargo.toml | 11 ++ Makefile | 47 ++++++++ grammar.y | 157 +++++++++++++++++++++++++ main.c | 19 ++++ src/dice.rs | 102 +++++++++++++++++ src/distribution.rs | 271 ++++++++++++++++++++++++++++++++++++++++++++ src/functions.rs | 51 +++++++++ src/instruction.rs | 68 +++++++++++ src/lib.rs | 174 ++++++++++++++++++++++++++++ src/number.rs | 98 ++++++++++++++++ src/ops.rs | 108 ++++++++++++++++++ src/result.rs | 226 ++++++++++++++++++++++++++++++++++++ src/stackframe.rs | 142 +++++++++++++++++++++++ src/yytext.rs | 99 ++++++++++++++++ tokens.lex | 97 ++++++++++++++++ 17 files changed, 1724 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 grammar.y create mode 100644 main.c create mode 100644 src/dice.rs create mode 100644 src/distribution.rs create mode 100644 src/functions.rs create mode 100644 src/instruction.rs create mode 100644 src/lib.rs create mode 100644 src/number.rs create mode 100644 src/ops.rs create mode 100644 src/result.rs create mode 100644 src/stackframe.rs create mode 100644 src/yytext.rs create mode 100644 tokens.lex diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e236f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +calc +lex.yy.* +grammar.tab.* +main.o +rust.h \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ecf0fe5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,48 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anydice" +version = "0.1.0" +dependencies = [ + "term_size", +] + +[[package]] +name = "libc" +version = "0.2.148" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" + +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..08f2c65 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "anydice" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["staticlib"] + +[dependencies] +term_size = "0.3.2" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..34992fe --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +CC := gcc +LIBRARIES := -lfl -lm -lreadline + +calc: main.o grammar.tab.o lex.yy.o target/release/libanydice.a + $(CC) -o calc $^ $(LIBRARIES) + +main.o: main.c grammar.tab.h + $(CC) -c $^ + +lex.yy.o: lex.yy.c + $(CC) -c $^ + +lex.yy.c: tokens.lex + flex -o lex.yy.c $^ + +grammar.tab.o: grammar.tab.c + $(CC) -c $^ + +grammar.tab.c: grammar.y rust.h + bison -d $< + +grammar.tab.h: grammar.y rust.h + bison -d $< + +.PHONY: rust +rust: + cargo build -r + cbindgen -o rust.h -l c + +rust.h: rust + +target/release/libanydice.a: rust + +.PHONY: clean +clean: + rm -f lex.yy.* + rm -f grammar.tab.* + rm -f main.o calc + rm -f rust.h + +.PHONY: sr +sr: grammar.y + bison -d -Wcounterexamples $< + +.PHONY: lint +lint: + cargo clippy --no-deps \ No newline at end of file diff --git a/grammar.y b/grammar.y new file mode 100644 index 0000000..29e0894 --- /dev/null +++ b/grammar.y @@ -0,0 +1,157 @@ +%{ + +#include +#include +#include +#include + +#include "rust.h" +#define YYSTYPE BSTNode + +int yyerror(char*); +extern Frame* current_frame; +extern Frame* main_frame; +extern Functions* global_functions; + +%} + +%token NUMBER IDENT ASSIGN SPACE BLOCKSTART BLOCKEND DICE RETURN FUNCTION DROP +%token PLUS MINUS MUL DIV POWER FLATMUL LESS LESSEQ EQ GREATEREQ GREATER +%token LEFT RIGHT +%token END ARGSEP + +%left LESS LESSEQ EQ GREATEREQ GREATER +%left PLUS MINUS +%left MUL DIV FLATMUL +%left NEG +%left BAR FACT +%right POWER + +%start Input +%% + +Input: + /* empty input */ + | Input Line +; + +Line: + END + // echo expressions to stdout + | Expression END { + if (current_frame == main_frame) + print_node(main_frame, $1, global_functions); + else + insert_instruction(current_frame, evaluate($1)); + } + // assignment + | IDENT ASSIGN Expression END { + if (current_frame == main_frame) + assign_node(main_frame, $1, $3, global_functions); + else + insert_instruction(current_frame, assign($1, $3)); + } + // return something + | RETURN Expression END { + if (current_frame == main_frame) + fprintf(stderr, "cannot return from repl\n"); + else + insert_instruction(current_frame, return_value($2)); + } + // return nothing + | RETURN END { + if (current_frame == main_frame) + fprintf(stderr, "cannot return from repl\n"); + else + insert_instruction(current_frame, return_nothing()); + } + + // function definition + | FUNCTION IDENT LEFT Arglist RIGHT BLOCKSTART END { + current_frame = make_frame_from_declaration($2, $4); + } + // and function finish + | BLOCKEND END { + add_function(global_functions, current_frame); + current_frame = main_frame; + } + + // memory management + | DROP IDENT END { + drop_bst($2, current_frame); + } +; + +Expression: + // terminal nodes: a literal number and a variable + NUMBER { $$ = yylval; } + | IDENT { $$ = yylval; } + | DICE { $$ = yylval; } + + // term separators + | Expression PLUS Expression { $$ = bst_add($1, $3); } + | Expression MINUS Expression { $$ = bst_sub($1, $3); } + + // there are a surprising number of ways to multiply + | Expression MUL Expression { $$ = bst_mul($1, $3); } + | LEFT Expression RIGHT LEFT Expression RIGHT { $$ = bst_mul($2, $5); } + | Expression FLATMUL Expression { $$ = bst_flatmul($1, $3); } + + // division + | Expression DIV Expression { $$ = bst_div($1, $3); } + + | Expression POWER Expression { $$ = bst_pow($1, $3); } + + | MINUS Expression %prec NEG { $$ = bst_neg($2); } + + // factorial, absolute value + | Expression FACT { $$ = bst_fact($1); } + | BAR Expression BAR { $$ = bst_abs($2); } + + // parenthesis + | LEFT Expression RIGHT { $$ = $2; } + + // function call + | IDENT LEFT Arglist RIGHT { + $$ = call_function($1, $3); + } + + // comparison operators + | Expression LESS Expression { + + } + | Expression LESSEQ Expression { + + } + | Expression EQ Expression { + + } + | Expression GREATEREQ Expression { + + } + | Expression GREATER Expression { + + } +; + +Arglist: + /* empty arg list */ { + $$ = no_arg(); + } + | NonEmptyArglist { + $$ = $1; + } +; + +NonEmptyArglist: + Expression { $$ = one_arg($1); } + | NonEmptyArglist ARGSEP Expression { + $$ = add_arg($1, $3); + } +; + +%% + +int yyerror(char *s) { + printf("%s\n", s); +} \ No newline at end of file diff --git a/main.c b/main.c new file mode 100644 index 0000000..42f398c --- /dev/null +++ b/main.c @@ -0,0 +1,19 @@ +#include +#include "rust.h" +#include "grammar.tab.h" + +Frame* current_frame; +Frame* main_frame; +Functions* global_functions; + +int main() { + main_frame = make_frame(yytext_from_cstr("main")); + current_frame = main_frame; + global_functions = make_global_functions(); + + if (yyparse() != 0) + fprintf(stderr, "error found.\n"); + + destroy_frame(main_frame); + destroy_global_functions(global_functions); +} \ No newline at end of file diff --git a/src/dice.rs b/src/dice.rs new file mode 100644 index 0000000..7ff64e2 --- /dev/null +++ b/src/dice.rs @@ -0,0 +1,102 @@ +use std::str::FromStr; +use std::convert::Infallible; +use std::mem::swap; + +use crate::result::MathResult; +use crate::distribution::Distribution; + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct Die { + num: u32, + sides: u32, +} +impl FromStr for Die { + type Err = MathResult; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split('d').collect(); + let num = match parts[0].parse() { + Ok(n) => n, + Err(_) => return Err(MathResult::ParseError), + }; + let sides = match parts[1].parse() { + Ok(n) => n, + Err(_) => return Err(MathResult::ParseError), + }; + + Ok(Self { + num, + sides, + }) + } +} +impl Die { + pub fn distribution(self) -> MathResult { + distribute(&self) + } +} + +/// Calculate the distribution of a given set of dice. This returns the resulting +/// vector of counts, along with the minimum roll. The second tuple field if used +/// for displaying the results +fn distribute(d: &Die) -> MathResult { + // "flat dice" is the dice, but flattened. it's just useful to strip away the unnecessary + // information + let flat_dice = vec![d.sides; d.num as usize]; + + // the minimum value is the length of flat_dice, as that is the value + // when everything rolls a one. this is used as part of the return value + let min = flat_dice.len(); + // the maximum is used for sizing the vectors. it is the sum of every + // dice's number of sides, which would be the maximum roll + let max = flat_dice.iter().sum::() as usize; + // the result and internal vectors are both initialized with counts of 0 + // for every roll. + let mut result = vec![0_usize; max-min+1]; + let mut internal = vec![0_usize; max-min+1]; + + // initialize result by putting ones in all the first die's rolls + let mut iter = flat_dice.into_iter(); + let init_sides = match iter.next() { + None => return MathResult::Ok(Distribution::new()), + Some(n) => n, + }; + for roll in 0..init_sides { + result[roll as usize] = 1; + } + + // the meat of the algorithm goes like this: + + // for each die... + for sides in iter { + // for each previous non-zero value we had... + for (prevroll, &prevcount) in result.iter().enumerate() { + if prevcount == 0 { + continue; + } + + // for each possible roll of the dice... + for roll in 1..=sides { + // increment the internal count by the previous count + let idx = prevroll + roll as usize-1; + internal[idx] = match internal[idx].checked_add(prevcount) { + Some(n) => n, + None => return MathResult::Overflow, + } + } + } + + // swap the two vectors to effectively write `internal` to `result` + swap(&mut result, &mut internal); + // fill internal with zeros, ready for the next loop + internal.fill(0); + } + + let iter = result.into_iter() + .enumerate() + .map(|(roll, count)| ((min + roll) as i32, count as u128)); + let result = Distribution::from_iter(iter); + + MathResult::Ok(result) +} \ No newline at end of file diff --git a/src/distribution.rs b/src/distribution.rs new file mode 100644 index 0000000..70860c6 --- /dev/null +++ b/src/distribution.rs @@ -0,0 +1,271 @@ +use std::collections::HashMap; +use std::hint::unreachable_unchecked; +use crate::number::{Integer, Number}; +use crate::result::{MathResult, IntoMath}; + +/// This struct is for deliberately putting a HashMap on the heap +/// this is done so that cbindgen can represent it as a single pointer +/// and it can be used in an ffi-safe manner. +#[allow(clippy::box_collection)] +#[repr(C)] +#[derive(Debug, Default, Clone)] +pub struct Distribution { + map: Box>, +} + +/// Combine two distributions by mapping the original distribution through a map function +/// The map operation is ?ed to propagate errors +fn map_distribution(src: Distribution, mut op: F) -> MathResult +where F: FnMut(Integer) -> MathResult { + let mut result = HashMap::with_capacity(src.map.len()); + + for (roll, count) in src { + let key = op(roll)?; + let entry = result.entry(key).or_insert(0); + *entry = count.checked_add(*entry).into_math(MathResult::Overflow)?; + } + + MathResult::Ok( Distribution::from_map(result) ) +} + +fn combine_distribution(a: Distribution, b: Distribution, mut op: F) -> MathResult +where F: FnMut(Integer, Integer) -> MathResult { + let mut result = HashMap::new(); // TODO! HashMap::with_capacity() + + for (roll1, count1) in a { + for (&roll2, &count2) in &b { + let key = op(roll1, roll2)?; + let value = count1.checked_mul(count2).into_math(MathResult::Overflow)?; + let entry = result.entry(key).or_insert(0); + *entry = value.checked_add(*entry).into_math(MathResult::Overflow)?; + } + } + + MathResult::Ok( Distribution::from_map(result) ) +} + +impl Distribution { + /// Return the default: an empty distribution, i.e. one that + /// does not allocate anything on the heap. This may be undesirable, + /// such as when you are using it as an "identitiy" element, it will + /// actually act like a nullity element, and all operation involving + /// it will return the "null distribution". For an identity distribution, + /// use Distribution::identity(). + pub fn new() -> Self { + Default::default() + } + + /// Return the additive identity distribution, i.e. the one where adding + /// any other distribution will return the other distribution + /// (not literally, mind you, just an equivalent distribution). + /// This *does* allocate. Specifically, it creates a distribution with + /// 0 having appeared once. + /// # A note on indirection + /// because this must be used in ffi, the HashMap (which already puts + /// its objects on the heap) must also be put in a box, meaning a single + /// read is three levels of indirection (box -> hashmap -> bucket -> object) + pub fn identitiy() -> Self { + let map = HashMap::from([(0, 1)]); + Self { + map: Box::new(map), + } + } + + pub fn from_map(map: HashMap) -> Self { + Self { + map: Box::new(map) + } + } + + pub fn add_int(self, rhs: Integer) -> MathResult { + // I don't want to define all of this in the second argument + let op = |roll: Integer| { + roll.checked_add(rhs).into_math(MathResult::Overflow) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + pub fn add_distribution(self, rhs: Self) -> MathResult { + let op = |roll1: Integer, roll2: Integer| { + roll1.checked_add(roll2).into_math(MathResult::Overflow) + }; + let distr = combine_distribution(self, rhs, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn sub_int(self, rhs: Integer) -> MathResult { + let op = |roll: Integer| { + roll.checked_sub(rhs).into_math(MathResult::Underflow) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + pub fn sub_distribution(self, rhs: Self) -> MathResult { + let op = |roll1: Integer, roll2: Integer| { + roll1.checked_sub(roll2).into_math(MathResult::Underflow) + }; + let distr = combine_distribution(self, rhs, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn mul_int(self, rhs: Integer) -> MathResult { + let op = |roll: Integer| { + roll.checked_mul(rhs).into_math(MathResult::Overflow) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + pub fn mul_distribution(self, rhs: Self) -> MathResult { + let op = |roll1: Integer, roll2: Integer| { + roll1.checked_mul(roll2).into_math(MathResult::Overflow) + }; + let distr = combine_distribution(self, rhs, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn flatmul(self, rhs: Integer) -> MathResult { + let mut result = Distribution::identitiy(); + let map_op = match rhs { + Integer::MIN ..= -1 => Self::sub_distribution, + 0 => return MathResult::Ok(Number::Dice(result)), + 1..=Integer::MAX => Self::add_distribution, + }; + let times = rhs.checked_abs().into_math(MathResult::Overflow)?; + for _ in 0..times { + result = match map_op(result, self.clone())? { + Number::Dice(d) => d, + Number::Int(_) => unsafe { unreachable_unchecked() }, + }; + } + MathResult::Ok(Number::Dice(result)) + } + + pub fn div_int(self, rhs: Integer) -> MathResult { + let op = |roll: Integer| { + roll.checked_div(rhs).into_math(MathResult::ZeroDivision) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + pub fn div_distribution(self, rhs: Self) -> MathResult { + let op = |roll1: Integer, roll2: Integer| { + roll1.checked_div(roll2).into_math(MathResult::ZeroDivision) + }; + let distr = combine_distribution(self, rhs, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn pow_int(self, rhs: Integer) -> MathResult { + let exp = u32::try_from(rhs).into_math(MathResult::OutOfRange)?; + let op = |roll: Integer| { + roll.checked_pow(exp).into_math(MathResult::Overflow) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + pub fn pow_distribution(self, rhs: Self) -> MathResult { + let op = |roll1: Integer, roll2: Integer| { + let exp = u32::try_from(roll2).into_math(MathResult::OutOfRange)?; + roll1.checked_pow(exp).into_math(MathResult::Overflow) + }; + let distr = combine_distribution(self, rhs, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn absolute(self) -> MathResult { + let op = |roll: Integer| { + roll.checked_abs().into_math(MathResult::Overflow) + }; + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } + + pub fn factorial(self) -> MathResult { + let op = |roll: Integer| { + if roll < 0 { + return MathResult::OutOfRange; + } + let mut result: Integer = 1; + + for n in 2..=roll { + result = result.checked_mul(n).into_math(MathResult::Overflow)?; + } + + MathResult::Ok(result) + }; + + let distr = map_distribution(self, op)?; + MathResult::Ok(Number::Dice(distr)) + } +} +/// Dispay implementation +impl Distribution { + pub fn display(&self) -> MathResult { + let mut rows = Vec::with_capacity(self.map.len()); + let total = self.map.values().sum::(); + + let just_len = self.map.keys() + .map(|n| n.to_string().len()) + .max() + .unwrap_or(0); + + // width is how much space we have to play with in the bars, its the total width of stdout... + let width = match term_size::dimensions_stdout() { + Some((width, _height)) => width, + None => return MathResult::PrintError, + } + // minus the width of the justification... + - just_len + // minus the width of the percentage itself, the % sign, and the " │ " separator. + - 7; + + let as_128 = width as u128; + let factor = + if let Some(&biggest_problem) = self.map.values().filter(|&&n| n > as_128).max() { + (width - just_len - 3) as f64 / biggest_problem as f64 + } else { + 1.0 + }; + + let mut distribution = self.map.iter() + .filter(|&(_, &count)| count > 0) + .map(|(&roll, &count)| (roll, count)) + .collect::< Vec<_> >(); + distribution.sort_unstable_by_key(|tuple| tuple.0); + + for (roll, count) in distribution { + let repeat_len = count as f64 * factor; + let bar = "・".repeat(repeat_len as usize); + let percent = 100.0 * count as f64 / total as f64; + let row = format!("{percent:>5.2}% {roll: as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.map.into_iter() + } +} +impl<'a> IntoIterator for &'a Distribution { + type Item = (&'a Integer, &'a u128); + type IntoIter = <&'a HashMap as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.map.iter() + } +} +impl FromIterator<(Integer, u128)> for Distribution { + fn from_iter(iter: T) -> Self + where T: IntoIterator { + Self { + map: Box::new(iter.into_iter().collect()) + } + } +} \ No newline at end of file diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 0000000..db5740d --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,51 @@ +use std::collections::HashMap; +use std::mem::{forget, take}; + +use crate::BSTNode; +use crate::yytext::YYText; +use crate::stackframe::Frame; + +pub type Functions = HashMap; + +#[no_mangle] +pub extern "C" +fn call_function(function_name: BSTNode, operands: BSTNode) -> BSTNode { + let args = match operands { + BSTNode::Arglist(/* box */args) => args, + other => Box::new(vec![other]), + }; + let fname = unsafe { function_name.identifier().unwrap_unchecked() }; + BSTNode::FunctionCall(fname, args) +} + +#[no_mangle] +pub extern "C" +fn add_function(functions: &mut Functions, frame: *const Frame) { + let mut f = unsafe { frame.read() }; + let name = take(&mut f.name); + functions.insert(name, f); +} + +#[no_mangle] +pub extern "C" +fn make_global_functions() -> *mut Functions { + let mut b = Box::::default(); + let result = b.as_mut() as *mut Functions; + forget(b); + result +} + +#[no_mangle] +pub extern "C" +fn destroy_global_functions(f: *mut Functions) { + drop( unsafe { Box::from_raw(f) } ); +} + +#[no_mangle] +pub extern "C" +fn insert_function(global_functions: *mut Functions, identifier: YYText) { + let name = identifier.clone(); + unsafe { + global_functions.as_mut().unwrap_unchecked() + }.insert(identifier, Frame::new(name)); +} \ No newline at end of file diff --git a/src/instruction.rs b/src/instruction.rs new file mode 100644 index 0000000..cd67826 --- /dev/null +++ b/src/instruction.rs @@ -0,0 +1,68 @@ +use crate::BSTNode; +use crate::yytext::YYText; + +#[repr(C)] +#[derive(Debug, Clone)] +pub enum Instruction { + Evaluate(BSTNode), + Assign(YYText, BSTNode), + Return(BSTNode), +} + +#[no_mangle] +pub extern "C" +fn evaluate(root: BSTNode) -> Instruction { + Instruction::Evaluate(root) +} + +#[no_mangle] +pub extern "C" +fn assign(ident: BSTNode, root: BSTNode) -> Instruction { + Instruction::Assign( + unsafe { ident.identifier().unwrap_unchecked() }, + root + ) +} + +#[no_mangle] +pub extern "C" +fn return_value(value: BSTNode) -> Instruction { + Instruction::Return(value) +} + +#[no_mangle] +pub extern "C" +fn return_nothing() -> Instruction { + Instruction::Return(BSTNode::Void) +} + +#[no_mangle] +pub extern "C" +fn add_arg(args: BSTNode, new: BSTNode) -> BSTNode { + let args = match (args, new) { + (BSTNode::Arglist(mut args1), BSTNode::Arglist(args2)) => { + args1.extend(*args2); + args1 + }, + (BSTNode::Arglist(mut args), other) + | (other, BSTNode::Arglist(mut args)) => { + args.push(other); + args + }, + (other1, other2) => Box::new(vec![other1, other2]), + }; + + BSTNode::Arglist(args) +} + +#[no_mangle] +pub extern "C" +fn no_arg() -> BSTNode { + BSTNode::Arglist(Box::default()) +} + +#[no_mangle] +pub extern "C" +fn one_arg(arg: BSTNode) -> BSTNode { + BSTNode::Arglist(Box::new(vec![arg])) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..eb02aec --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,174 @@ +#![feature(try_trait_v2)] + +// modules +mod dice; +mod distribution; +mod instruction; +mod stackframe; +mod yytext; +mod ops; +mod result; +mod number; +mod functions; + +use dice::Die; +use functions::Functions; +use yytext::YYText; +use stackframe::Frame; +use result::MathResult; +use number::{Number, Integer}; + +#[repr(C)] +#[derive(Debug, Clone)] +pub enum BSTNode { + Literal(YYText), + Variable(YYText), + // called BSTDie becaues cbindgen will generate + // an enum called Die twice otherwise. this avoids that conflict + BSTDie(YYText), + + /// Unary operations like negation and factorial + Unary { + op: UnaryOperation, + operand: Box, + }, + /// Binary Operations like plus and subtract + Binary { + op: BinaryOperation, + left: Box, + right: Box, + }, + + /// This is funky, it's meant to be an arg list + Arglist(Box>), + FunctionCall(YYText, Box>), + + /// this is the argument to an empty return + Void, +} +impl BSTNode { + pub fn identifier(self) -> Option { + match self { + Self::Variable(text) => Some(text), + _ => None, + } + } + + pub fn args(self) -> Option> { + match self { + Self::Arglist(args) => Some(*args), + _ => None, + } + } +} +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum UnaryOperation { + Neg, + Abs, + Fact, +} +impl UnaryOperation { + fn eval(self, operand: Number) -> MathResult { + match self { + Self::Neg => operand.checked_neg(), + Self::Abs => operand.checked_abs(), + Self::Fact => factorial(operand), + } + } +} + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum BinaryOperation { + /// Plus + Add, + /// Minus + Sub, + /// Multiply + Mul, + /// In theory, multiplication is repeated addition, however + /// normal multiplication multiplies the rolls while keeping + /// the counts the same. Flatmul is repeated addition for dice + Flatmul, + /// Divide + Div, + /// Exponentiation + Pow, +} +impl BinaryOperation { + fn eval(self, left: Number, right: Number) -> MathResult { + match self { + Self::Add => left.checked_add(right), + Self::Sub => left.checked_sub(right), + Self::Mul => left.checked_mul(right), + Self::Flatmul => left.checked_flatmul(right), + Self::Div => left.checked_div(right), + Self::Pow => left.checked_pow(right), + } + } +} + +#[no_mangle] +pub extern "C" +fn bst_eval(root: BSTNode, frame: &Frame, function_list: &Functions) -> MathResult { + match root { + BSTNode::Literal(s) => match s.as_str().parse() { + Ok(n) => MathResult::Ok(Number::Int(n)), + Err(_) => MathResult::BadNumber, + }, + BSTNode::BSTDie(d) => { + let die = match d.as_str().parse::() { + Ok(d) => d, + Err(_) => return MathResult::ParseError, + }; + MathResult::Ok(Number::Dice(die.distribution()?)) + }, + BSTNode::Variable(ident) => frame.lookup(&ident), + BSTNode::Binary { op, left, right } => { + let left = bst_eval(*left, frame, function_list)?; + let right = bst_eval(*right, frame, function_list)?; + op.eval(left, right) + }, + BSTNode::Unary { op, operand } => { + let operand = bst_eval(*operand, frame, function_list)?; + op.eval(operand) + }, + BSTNode::Arglist(_) => panic!("never"), // TODO: std::hint::unreachable_unchecked() + BSTNode::FunctionCall(name, args) => { + let f = match function_list.get(&name) { + Some(f) => f, + None => return MathResult::FunctionNotFound, + }; + f.call(*args, frame, function_list) + }, + BSTNode::Void => MathResult::Void, + } +} + +/// simple, c-accessible drop implementation +#[no_mangle] +pub extern "C" +fn drop_bst(node: BSTNode, current_frame: &mut Frame) { + let ident = unsafe { node.identifier().unwrap_unchecked() }; + current_frame.forget(ident) +} + +/// this is a really weird function because its in lib.rs +fn factorial(n: Number) -> MathResult { + let n = match n { + Number::Int(n) if n > 0 => n, + Number::Int(_) => return MathResult::OutOfRange, + Number::Dice(d) => return d.factorial(), + }; + + let mut result: Integer = 1; + for i in 2..=n { + result = match result.checked_mul(i) { + Some(next) => next, + None => return MathResult::Overflow, + }; + } + + MathResult::Ok(Number::Int(result)) +} \ No newline at end of file diff --git a/src/number.rs b/src/number.rs new file mode 100644 index 0000000..b800d1a --- /dev/null +++ b/src/number.rs @@ -0,0 +1,98 @@ +use std::fmt::Display; + +use crate::distribution::Distribution; +use crate::result::{MathResult, IntoMath}; + +pub type Integer = i32; + +#[repr(C)] +#[derive(Debug, Clone)] +pub enum Number { + Int(i32), + Dice(Distribution) +} +impl Number { + pub fn checked_add(self, rhs: Number) -> MathResult { + match (self, rhs) { + (Self::Int(n1), Self::Int(n2)) => n1.checked_add(n2).map(Number::Int).into_math(MathResult::Overflow), + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.add_int(n), + (Self::Dice(d1), Self::Dice(d2)) => d1.add_distribution(d2), + } + } + pub fn checked_sub(self, rhs: Number) -> MathResult { + match (self, rhs) { + (Self::Int(n1), Self::Int(n2)) => n1.checked_sub(n2).map(Number::Int).into_math(MathResult::Overflow), + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.sub_int(n), + (Self::Dice(d1), Self::Dice(d2)) => d1.sub_distribution(d2), + + } + } + + pub fn checked_mul(self, rhs: Number) -> MathResult { + match (self, rhs) { + (Self::Int(n1), Self::Int(n2)) => n1.checked_mul(n2).map(Number::Int).into_math(MathResult::Underflow), + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.mul_int(n), + (Self::Dice(d1), Self::Dice(d2)) => d1.mul_distribution(d2) + } + } + pub fn checked_flatmul(self, rhs: Number) -> MathResult { + match (self, rhs) { + // flatmul isn't distinct from regular multiplication for two numbers or two dice + (Self::Int(n1), Self::Int(n2)) => n1.checked_mul(n2).map(Number::Int).into_math(MathResult::Underflow), + (Self::Dice(d1), Self::Dice(d2)) => d1.mul_distribution(d2), + + // this is what flatmul really is: adding a distribution to itself `rhs` times + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.flatmul(n), + } + } + pub fn checked_div(self, rhs: Number) -> MathResult { + match (self, rhs) { + (Self::Int(n1), Self::Int(n2)) => n1.checked_div(n2).map(Number::Int).into_math(MathResult::ZeroDivision), + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.div_int(n), + (Self::Dice(d1), Self::Dice(d2)) => d1.div_distribution(d2) + } + } + + pub fn checked_pow(self, rhs: Number) -> MathResult { + match (self, rhs) { + (Self::Int(n1), Self::Int(n2)) => { + let exp = match n2.try_into() { + Ok(n) => n, + Err(_) => return MathResult::OutOfRange, + }; + n1.checked_pow(exp).map(Number::Int).into_math(MathResult::Overflow) + }, + (Self::Int(n), Self::Dice(d)) + | (Self::Dice(d), Self::Int(n)) => d.pow_int(n), + (Self::Dice(d1), Self::Dice(d2)) => d1.pow_distribution(d2) + } + } + + pub fn checked_abs(self) -> MathResult { + match self { + Self::Int(this) => this.checked_abs().map(Number::Int).into_math(MathResult::Overflow), + Self::Dice(d) => d.absolute(), + } + } + + pub fn checked_neg(self) -> MathResult { + match self { + Self::Int(this) => this.checked_neg().map(Number::Int).into_math(MathResult::Underflow), + Self::Dice(d) => d.mul_int(-1), + } + } +} + +impl Display for Number { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Int(n) => write!(f,"{n}"), + Self::Dice(d) => write!(f, "{}", d.display()), + } + } +} \ No newline at end of file diff --git a/src/ops.rs b/src/ops.rs new file mode 100644 index 0000000..5932e15 --- /dev/null +++ b/src/ops.rs @@ -0,0 +1,108 @@ +use crate::{BSTNode, BinaryOperation, UnaryOperation}; +use crate::yytext::yytext_from_cstr; +use std::ffi::c_char; + +#[no_mangle] +pub extern "C" +fn bst_number(ptr: *mut c_char) -> BSTNode { + BSTNode::Literal(yytext_from_cstr(ptr)) +} + +#[no_mangle] +pub extern "C" +fn bst_add(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Add, + left: Box::new(left), + right: Box::new(right) + } +} + +#[no_mangle] +pub extern "C" +fn bst_sub(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Sub, + left: Box::new(left), + right: Box::new(right), + } +} + +#[no_mangle] +pub extern "C" +fn bst_mul(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Mul, + left: Box::new(left), + right: Box::new(right), + } +} + +#[no_mangle] +pub extern "C" +fn bst_flatmul(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Flatmul, + left: Box::new(left), + right: Box::new(right), + } +} + +#[no_mangle] +pub extern "C" +fn bst_div(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Div, + left: Box::new(left), + right: Box::new(right), + } +} + +#[no_mangle] +pub extern "C" +fn bst_pow(left: BSTNode, right: BSTNode) -> BSTNode { + BSTNode::Binary { + op: BinaryOperation::Pow, + left: Box::new(left), + right: Box::new(right), + } +} + +#[no_mangle] +pub extern "C" +fn bst_neg(operand: BSTNode) -> BSTNode { + BSTNode::Unary { + op: UnaryOperation::Neg, + operand: Box::new(operand) + } +} + +#[no_mangle] +pub extern "C" +fn bst_fact(operand: BSTNode) -> BSTNode { + BSTNode::Unary { + op: UnaryOperation::Fact, + operand: Box::new(operand) + } +} + +#[no_mangle] +pub extern "C" +fn bst_abs(operand: BSTNode) -> BSTNode { + BSTNode::Unary { + op: UnaryOperation::Abs, + operand: Box::new(operand) + } +} + +#[no_mangle] +pub extern "C" +fn bst_variable(ident: *mut c_char) -> BSTNode { + BSTNode::Variable( yytext_from_cstr(ident) ) +} + +#[no_mangle] +pub extern "C" +fn bst_dice(ident: *mut c_char) -> BSTNode { + BSTNode::BSTDie( yytext_from_cstr(ident) ) +} \ No newline at end of file diff --git a/src/result.rs b/src/result.rs new file mode 100644 index 0000000..60f6acc --- /dev/null +++ b/src/result.rs @@ -0,0 +1,226 @@ +use std::convert::Infallible; +use std::hint::unreachable_unchecked; +/// Define the FFI-safe MathResult type +/// this would be a regular type alias, but a theoretical +/// Result wouldn't be FFI-safe because +/// Result is not #[repr(C)] even if both T +/// and E are. + +use std::ops::{FromResidual, Try, ControlFlow}; +use std::fmt::Display; + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub enum MathResult { + /// Ok(T) is the Ok variation. Analagous to + /// std::result::Result::Ok. + Ok(T), + /// This is a parse error, specifically when trying to parse + /// a number. + BadNumber, + /// Returned when attempting to call a nonexistent function + FunctionNotFound, + /// An intermediate operation resulted in integer overflow. + /// Also returned when trying to calculate the negative of + /// `i32::MIN` + Overflow, + /// An operation had out of bounds operands, like the factorial + /// of a negative. + OutOfRange, + /// Not enough arguments were passed to a function + TooFewArgs, + /// Too many arguments were passed to a function + TooManyArgs, + /// An operation resulted in integer underflow. + Underflow, + /// One of the referenced variables in an expression has not been + /// assigned yet. Different to FunctionNotFound because that occurs + /// for functions. + VariableNotFound, + /// This is no an error per-se, but rather the result of return with + /// no expression to return. This is early-returned to simulate the + /// fact that any expression involving null would also return null. + Void, + /// An error occured while trying to parse an object passed by Bison + ParseError, + /// An error occured while trying to print an object. This basically + /// never occurs unless stdout has been redirected, but is useful + /// for a no-panic program that involves io. + PrintError, + /// The denominator of a division was 0. + ZeroDivision, +} + +impl FromResidual> for MathResult { + /// This implementation of residual is a bit weird. The residual + /// is of type `MathResult` because the error variants are effectively + /// untyped, in that they do not have any generic or concrete types. + /// # Undefined behaviour + /// This function assumes that it will only be called by Rust to + /// reconstitute a `MathResult` from one of its error variants, and + /// as such it is **Undefined Behaviour** to call this on a `MathResult::Ok` + fn from_residual(this: MathResult) -> Self { + match this { + MathResult::Ok(_) => unsafe { unreachable_unchecked() }, + MathResult::BadNumber => Self::BadNumber, + MathResult::FunctionNotFound => Self::FunctionNotFound, + MathResult::Overflow => Self::Overflow, + MathResult::OutOfRange => Self::OutOfRange, + MathResult::TooFewArgs => Self::TooFewArgs, + MathResult::TooManyArgs => Self::TooManyArgs, + MathResult::Underflow => Self::Underflow, + MathResult::VariableNotFound => Self::VariableNotFound, + MathResult::Void => Self::Void, + MathResult::ParseError => Self::ParseError, + MathResult::PrintError => Self::PrintError, + MathResult::ZeroDivision => Self::ZeroDivision, + } + } +} + +impl Try for MathResult { + type Output = T; + type Residual = Self; + + fn from_output(data: T) -> Self { + Self::Ok(data) + } + + fn branch(self) -> ControlFlow { + // this is a weird match, but essentialy the Ok data can + // get `Continue`d, while others get `Break`ed. + match self { + MathResult::Ok(data) => ControlFlow::Continue(data), + other => ControlFlow::Break(other), + } + } +} + +/// For a given Result>, convert into a +/// MathResult by turning Ok(t) into MathResult::Ok(t) and Err(n) +/// into Self::n. E in the Result must be of type MathResult +/// to avoid the possibility of Err(Ok(_)). +impl From< Result< OkType, MathResult > > for MathResult { + fn from(value: Result< OkType, MathResult >) -> MathResult { + match value { + Ok(data) => Self::Ok(data), + // this is unreachable because this is Err(Ok(Infalliable)) + Err(MathResult::Ok(_)) => unsafe { unreachable_unchecked() }, + Err(MathResult::BadNumber) => Self::BadNumber, + Err(MathResult::FunctionNotFound) => Self::FunctionNotFound, + Err(MathResult::OutOfRange) => Self::OutOfRange, + Err(MathResult::Overflow) => Self::Overflow, + Err(MathResult::TooFewArgs) => Self::TooFewArgs, + Err(MathResult::TooManyArgs) => Self::TooManyArgs, + Err(MathResult::Underflow) => Self::Underflow, + Err(MathResult::VariableNotFound) => Self::VariableNotFound, + Err(MathResult::ZeroDivision) => Self::ZeroDivision, + Err(MathResult::Void) => Self::Void, + Err(MathResult::ParseError) => Self::ParseError, + Err(MathResult::PrintError) => Self::PrintError, + } + } +} + +/// For a given MathResult, turn it into a +/// Result by mapping Self::Ok(data) to +/// Ok(data), and any error value to Err(value) +impl From< MathResult > for Result> { + fn from(val: MathResult) -> Self { + match val { + MathResult::Ok(data) => Ok(data), + MathResult::BadNumber => Err(MathResult::BadNumber), + MathResult::FunctionNotFound => Err(MathResult::FunctionNotFound), + MathResult::OutOfRange => Err(MathResult::OutOfRange), + MathResult::Overflow => Err(MathResult::Overflow), + MathResult::TooFewArgs => Err(MathResult::TooFewArgs), + MathResult::TooManyArgs => Err(MathResult::TooManyArgs), + MathResult::Underflow => Err(MathResult::Underflow), + MathResult::VariableNotFound => Err(MathResult::VariableNotFound), + MathResult::ZeroDivision => Err(MathResult::ZeroDivision), + MathResult::Void => Err(MathResult::Void), + MathResult::ParseError => Err(MathResult::ParseError), + MathResult::PrintError => Err(MathResult::PrintError), + } + } +} + +impl FromIterator< MathResult > for MathResult +where Collection: FromIterator { + fn from_iter(iter: I) -> MathResult + where I: IntoIterator< Item = MathResult > { + let result_iterator = iter.into_iter().map( + // this hellish generic selects the Into implementation that converts + // a MathResult into a Result>, so that I + // can use Result's implementation of FromIterator later. + as Into< Result> >>::into + ); + // Use Result's implementation of FromIterator to build up the + // collection as a Result. + let collected_result: Result> = result_iterator.collect(); + // convert the result into a MathResult. The Err variant is + // of type MathResult so this is a flawless conversion. + collected_result.into() + } +} + + +impl Display for MathResult +where T: Display { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // pretty self-explanatory. write the right message for each variant + match self { + Self::Ok(data) => write!(f, "{data}"), + Self::BadNumber => write!(f, "unparseable number"), + Self::FunctionNotFound => write!(f, "function not found"), + Self::OutOfRange => write!(f, "out of range"), + Self::Overflow => write!(f, "integer overflow"), + Self::TooFewArgs => write!(f, "too few arguments"), + Self::TooManyArgs => write!(f, "too many arguments"), + Self::Underflow => write!(f, "integer underflow"), + Self::VariableNotFound => write!(f, "variable not found"), + Self::ZeroDivision => write!(f, "division by zero"), + Self::Void => write!(f, "(void)"), + Self::ParseError => write!(f, "parse error"), + Self::PrintError => write!(f, "printing error") + } + } +} + +mod seal_mod { + /// This is just a sanity check to make sure I never try to ipmlement + /// IntoMath elsewhere in the codebase. + pub trait Sealed {} +} +/// Ugly hack to simplify moving between rust's error-handling types `Option` +/// and `Result` and my `MathResult`. +/// This is `Sealed` for sanity reasons. +pub trait IntoMath: seal_mod::Sealed { + type Target; + + fn into_math(self, fallback: MathResult) -> MathResult; +} + +impl seal_mod::Sealed for Option {} +impl IntoMath for Option { + type Target = T; + + fn into_math(self, fallback: MathResult) -> MathResult { + match self { + Some(data) => MathResult::Ok(data), + None => fallback, + } + } +} + +impl seal_mod::Sealed for Result {} +impl IntoMath for Result { + type Target = T; + + fn into_math(self, fallback: MathResult) -> MathResult { + match self { + Ok(data) => MathResult::Ok(data), + Err(_) => fallback, + } + } +} \ No newline at end of file diff --git a/src/stackframe.rs b/src/stackframe.rs new file mode 100644 index 0000000..f612865 --- /dev/null +++ b/src/stackframe.rs @@ -0,0 +1,142 @@ +use std::mem::{forget, take}; +use std::collections::HashMap; + +use crate::functions::Functions; +use crate::{Number, bst_eval, BSTNode}; +use crate::result::{MathResult, IntoMath}; +use crate::yytext::YYText; +use crate::instruction::Instruction; + +#[derive(Default, Debug)] +pub struct Frame { + pub name: YYText, + args: Vec, + locals: HashMap, + instructions: Vec, +} +impl Frame { + pub fn new(name: YYText) -> Self { + Self { + name, + ..Default::default() + } + } + + fn from_args(source: &Frame, args: Vec, frame: &Frame, function_list: &Functions) -> MathResult { + if args.len() < source.args.len() { + return MathResult::TooFewArgs; + } + if args.len() > source.args.len() { + return MathResult::TooManyArgs; + } + + let args = args.into_iter() + .map(|root| bst_eval(root, frame, function_list)) + .collect::< MathResult> >()?; + + let mut result = Self::default(); + + for (name, rhs) in source.args.iter().zip(args) { + result.locals.insert(name.clone(), rhs); + } + result.instructions = source.instructions.clone(); + MathResult::Ok(result) + } + + pub fn execute(mut self, function_list: &Functions) -> MathResult { + // take the instruction list to allow a &self in the loop. otherwise + // rust complains of ref to partially moved self + let instructions = take(&mut self.instructions); + for inst in instructions { + match inst { + Instruction::Assign(name, node) => { + let rhs = bst_eval(node, &self, function_list)?; + self.locals.insert(name, rhs); + } + Instruction::Return(root) => return bst_eval(root, &self, function_list), + Instruction::Evaluate(_) => (), + } + } + MathResult::Void + + } + + pub fn lookup(&self, ident: &YYText) -> MathResult { + self.locals.get(ident) + .map(Clone::clone) + .into_math(MathResult::VariableNotFound) + } + + pub fn assign(&mut self, ident: YYText, value: Number) { + self.locals.insert(ident, value); + } + + pub fn call(&self, args: Vec, frame: &Frame, function_list: &Functions) -> MathResult { + let child = Frame::from_args(self, args, frame, function_list)?; + child.execute(function_list) + } + + pub fn forget(&mut self, ident: YYText) { + self.locals.remove(&ident); + } +} + +#[no_mangle] +pub extern "C" +fn insert_instruction(f: &mut Frame, value: Instruction) { + f.instructions.push(value); +} + +#[no_mangle] +pub extern "C" +fn print_node(f: &Frame, root: BSTNode, function_list: &Functions) { + println!("Result: {}", bst_eval(root, f, function_list)); +} + +#[no_mangle] +pub extern "C" +fn assign_node(f: &mut Frame, ident: BSTNode, root: BSTNode, function_list: &Functions) { + match bst_eval(root, f, function_list) { + MathResult::Ok(n) => { f.locals.insert(ident.identifier().unwrap(), n); }, + problem => eprintln!("Result: {problem}"), + } +} + +/// this makes the main frame for the app to run in. +/// #[allow(unused_allocation)] because rust thinks i'm putting this +/// on the heap for no reason +#[allow(unused_allocation)] +#[no_mangle] +pub extern "C" +fn make_frame(name: YYText) -> *mut Frame { + let mut b = Box::new( Frame::new(name) ); + let result = b.as_mut() as *mut Frame; + forget(b); + result +} + +/// Make a frame from a bst node. the provided node +/// must be of the variable variant so that it's +/// identifier can be extracted +#[no_mangle] +pub extern "C" +fn make_frame_from_declaration(name: BSTNode, args: BSTNode) -> *mut Frame { + let name = unsafe { name.identifier().unwrap_unchecked() }; + let args = unsafe { args.args().unwrap_unchecked() } + .into_iter() + .filter_map(BSTNode::identifier) + .collect(); + let mut result = Frame::new(name); + result.args = args; + let mut b = Box::new(result); + let result = b.as_mut() as *mut Frame; + forget(b); + result +} + +/// Destroy a frame by reclaiming the original box +#[no_mangle] +pub extern "C" +fn destroy_frame(frame: *mut Frame) { + drop( unsafe { Box::from_raw(frame) } ); +} diff --git a/src/yytext.rs b/src/yytext.rs new file mode 100644 index 0000000..957baed --- /dev/null +++ b/src/yytext.rs @@ -0,0 +1,99 @@ +use std::mem::forget; +use std::fmt::Debug; +use std::hash::Hash; +use std::ffi::{CString, c_char, CStr}; + +#[repr(C)] +pub struct YYText { + ptr: *mut u8, + len: usize, + cap: usize, +} +impl YYText { + pub fn new(mut s: String) -> Self { + // decompose a String into its parts + let ptr = s.as_mut_ptr(); + let len = s.len(); + let cap = s.capacity(); + let result = Self { ptr, len, cap }; + + // make sure s doesn't get dropped. a bit unsafe + // since the pointer returned by as_mut_ptr() isn't + // guaranteed to stay safe after forgetting, but it + // seems to work well enough + forget(s); + + result + } + + pub fn as_str(&self) -> &str { + let as_slice = unsafe { std::slice::from_raw_parts(self.ptr, self.len) }; + let as_str = std::str::from_utf8(as_slice).expect("invalid utf8"); + as_str + } +} +/// When dropped, reconstitute the String and make sure +/// that it gets freed correctly +impl Drop for YYText { + fn drop(&mut self) { + let s = unsafe { String::from_raw_parts(self.ptr, self.len, self.cap) }; + drop(s); + } +} +/// Debug as str but include the pointer +impl Debug for YYText { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?} @ {:?}", self.as_str(), self.ptr) + } +} +impl Clone for YYText { + fn clone(&self) -> Self { + Self::new( self.as_str().to_owned() ) + } +} +impl Default for YYText { + fn default() -> Self { + Self { + ptr: std::ptr::null_mut(), + len: 0, + cap: 0, + } + } +} + +/// PartialEq as str +impl PartialEq for YYText { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} +/// Eq as str +impl Eq for YYText {} + +/// Hash as str +impl Hash for YYText { + fn hash(&self, state: &mut H) + where H: std::hash::Hasher { + self.as_str().hash(state) + } +} + +pub fn string_from_cstr(src: *mut c_char) -> String { + let c_str = unsafe { CStr::from_ptr(src) }; + let c_string = CString::from(c_str); + c_string.into_string() + // bad error handling + .expect("could not convert to string") +} +/// More evil pointer manipulation to turn a char* into a YYText +/// basically, make a YYText that is equivalent to +/// ``` +/// reinterp_cast(yytext) +/// ``` +/// then clone that, then forget the original since it can't be dropped +#[no_mangle] +pub extern "C" +fn yytext_from_cstr(src: *mut c_char) -> YYText { + let string = string_from_cstr(src); + YYText::new(string) +} \ No newline at end of file diff --git a/tokens.lex b/tokens.lex new file mode 100644 index 0000000..b7dbb6a --- /dev/null +++ b/tokens.lex @@ -0,0 +1,97 @@ +%{ +#include // strncmp + +#include "rust.h" // bst_* functions +#define YYSTYPE BSTNode +#include "grammar.tab.h" // PLUS MINUS etc. + +extern YYSTYPE yylval; +extern Frame *current_frame; + +#include +#include +#define YY_INPUT(buf,result,max_size) result = mygetinput(buf, max_size); + +static int mygetinput(char *buf, int size) { + char *line; + + if (feof(yyin)) + return YY_NULL; + + line = readline("> "); + + if(line == NULL) + return YY_NULL; + + size_t len = strnlen(line, size); + if (len > size-2) + return YY_NULL; + + snprintf(buf, size, "%s\n", line); + + add_history(line); + free(line); + + return strlen(buf); +} + +%} + +%option noyywrap + +white [ \t]+ +integer [0-9]+ +ident [a-zA-Z][a-zA-Z0-9_]* + +%% + +{white} { ; } + +{integer}"d"{integer} { + yylval = bst_dice(yytext); + return DICE; +} +{integer} { + yylval = bst_number(yytext); + return NUMBER; +} + +{ident} { + // check for keywords + // note to self: don't pass yyleng as the max length, + // because then things like "ret" would pass as "return" + if (strncmp(yytext, "return", 6) == 0) { + return RETURN; + } + if (strncmp(yytext, "function", 8) == 0) { + return FUNCTION; + } + if (strncmp(yytext, "drop", 4) == 0) { + return DROP; + } + yylval = bst_variable(yytext); + return IDENT; +} + +"+" return PLUS; +"-" return MINUS; +"**" return FLATMUL; +"*" return MUL; +"/" return DIV; +"^" return POWER; +"(" return LEFT; +")" return RIGHT; +"!" return FACT; +"|" return BAR; +"\n" return END; +"<=" return LESSEQ; +"<" return LESS; +">=" return GREATEREQ; +">" return GREATER; +"==" return EQ; +"=" return ASSIGN; +"," return ARGSEP; +"{" return BLOCKSTART; +"}" return BLOCKEND; + +%% \ No newline at end of file