From 276d1f8eef130d88c133c5868c078b265ad28b61 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 28 Oct 2024 16:36:01 +1100 Subject: [PATCH 01/20] Experiments; numeric model 'analytic-engine' --- Cargo.lock | 108 +++++++- Cargo.toml | 2 +- experiments/analytic-engine/Cargo.toml | 13 + experiments/analytic-engine/src/lib.rs | 351 ++++++++++++++++++++++++ experiments/analytic-engine/src/main.rs | 5 + 5 files changed, 474 insertions(+), 5 deletions(-) create mode 100644 experiments/analytic-engine/Cargo.toml create mode 100644 experiments/analytic-engine/src/lib.rs create mode 100644 experiments/analytic-engine/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 04843393..d07cc786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "analytic-engine" +version = "0.1.0" +dependencies = [ + "bitflags 2.6.0", + "num-bigint", + "num-traits", +] + [[package]] name = "anes" version = "0.1.6" @@ -90,6 +99,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bumpalo" version = "3.16.0" @@ -166,7 +181,7 @@ checksum = "223163f58c9a40c3b0a43e1c4b50a9ce09f007ea2cb1ec258a687945b4b7929f" dependencies = [ "anstream", "anstyle", - "bitflags", + "bitflags 1.3.2", "clap_lex", "strsim", ] @@ -307,6 +322,7 @@ version = "0.1.0" dependencies = [ "criterion", "doodle", + "encoding", ] [[package]] @@ -315,6 +331,70 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + [[package]] name = "errno" version = "0.3.0" @@ -486,6 +566,26 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -558,7 +658,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" dependencies = [ "bit-set", - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "num-traits", @@ -658,7 +758,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -702,7 +802,7 @@ version = "0.37.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", diff --git a/Cargo.toml b/Cargo.toml index 12f1909d..576df92a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "generated/", "doodle-formats/"] +members = [".", "generated/", "doodle-formats/", "experiments/analytic-engine/"] [package] name = "doodle" diff --git a/experiments/analytic-engine/Cargo.toml b/experiments/analytic-engine/Cargo.toml new file mode 100644 index 00000000..5250dfd5 --- /dev/null +++ b/experiments/analytic-engine/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "analytic-engine" +version = "0.1.0" +edition = "2021" + +[lib] +name = "analytic_engine" +bench = false + +[dependencies] +num-bigint = "0.4" +num-traits = "0.2" +bitflags = "2.6" diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs new file mode 100644 index 00000000..74f7de73 --- /dev/null +++ b/experiments/analytic-engine/src/lib.rs @@ -0,0 +1,351 @@ +use num_bigint::BigInt; +use num_traits::{One, Signed, Zero}; + +pub type Number = BigInt; + +/// Standalone ground operations on two numeric arguments +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BasicBinOp { + Add, + Sub, + Mul, + Div, + Rem, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BasicUnaryOp { + Negate, + AbsVal, +} + +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] +pub enum NumRep { + Abstract, + Concrete { + is_signed: bool, + bit_width: BitWidth, + }, +} + +impl NumRep { + pub const I8: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits8, + }; + pub const I16: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits16, + }; + pub const I32: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits32, + }; + pub const I64: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits64, + }; + + pub const U8: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits8, + }; + pub const U16: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits16, + }; + pub const U32: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits32, + }; + pub const U64: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits64, + }; +} + +impl NumRep { + pub const fn is_abstract(&self) -> bool { + matches!(self, NumRep::Abstract) + } +} + +/// Representative min and max bounds for a numeric type +pub struct Bounds { + min: Number, + max: Number, +} + +macro_rules! bounds_of { + ( $t:ty ) => { + (Number::from(<$t>::MIN), Number::from(<$t>::MAX)) + }; +} + +impl NumRep { + fn as_bounds(&self) -> Option { + let (min, max) = match self { + NumRep::Abstract => return None, + &NumRep::U8 => bounds_of!(u8), + &NumRep::U16 => bounds_of!(u16), + &NumRep::U32 => bounds_of!(u32), + &NumRep::U64 => bounds_of!(u64), + &NumRep::I8 => bounds_of!(i8), + &NumRep::I16 => bounds_of!(i16), + &NumRep::I32 => bounds_of!(i32), + &NumRep::I64 => bounds_of!(i64), + }; + Some(Bounds { min, max }) + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] +pub enum BitWidth { + Bits8, + Bits16, + Bits32, + Bits64, +} + +#[derive(Clone, PartialEq, Debug)] +pub struct TypedConst(BigInt, NumRep); + +impl std::fmt::Display for TypedConst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let n = &self.0; + match self.1 { + NumRep::U8 => write!(f, "{}u8", n), + NumRep::U16 => write!(f, "{}u16", n), + NumRep::U32 => write!(f, "{}u32", n), + NumRep::U64 => write!(f, "{}u64", n), + NumRep::I8 => write!(f, "{}i8", n), + NumRep::I16 => write!(f, "{}i16", n), + NumRep::I32 => write!(f, "{}i32", n), + NumRep::I64 => write!(f, "{}i64", n), + NumRep::Abstract => write!(f, "{}??", n), + } + } +} + +impl TypedConst { + pub fn is_abstract(&self) -> bool { + self.1.is_abstract() + } + + /// Returns `true` if `self` is representable, which is true if either: + /// - The `NumRep` is `Abstract` + /// - The `NumRep` is concrete and `n` is in the bounds of the `NumRep` + pub fn is_representable(&self) -> bool { + let TypedConst(ref n, rep) = self; + if let Some(bounds) = rep.as_bounds() { + n >= &bounds.min && n <= &bounds.max + } else { + debug_assert!(rep.is_abstract()); + true + } + } + + pub fn as_raw_value(&self) -> &BigInt { + &self.0 + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum Value { + Const(TypedConst), + Opt(Option>), +} + +impl Value { + /// Returns `true` if `self` is representable: + /// - If `self` is a constant value, it must itself be representable + /// - If `self` is Some(x), `x` must be representable + /// + /// `None` is always representable. + pub fn is_representable(&self) -> bool { + match self { + Value::Const(c) => c.is_representable(), + Value::Opt(value) => value.as_deref().map_or(true, Value::is_representable), + } + } + + pub fn as_const(&self) -> Option<&TypedConst> { + match self { + Value::Const(c) => Some(c), + Value::Opt(value) => value.as_deref().and_then(Value::as_const), + } + } +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Const(num) => write!(f, "{num}"), + Value::Opt(None) => write!(f, "None"), + Value::Opt(Some(x)) => write!(f, "Some({})", x), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct BinOp { + op: BasicBinOp, + // If None, picks either the common rep of the two arguments or Abstract if they disagree + out_rep: Option, +} + +#[derive(Clone, Copy, Debug)] +pub struct UnaryOp { + op: BasicUnaryOp, + // If None, will pick the same type as the input (even if this produces a temporary unrepresentable) + out_rep: Option, +} + +#[derive(Clone, Debug)] +pub enum Expr { + Const(TypedConst), + BinOp(BinOp, Box, Box), + UnaryOp(UnaryOp, Box), + Cast(NumRep, Box), + TryUnwrap(Box), +} + +#[derive(Debug)] +pub enum EvalError { + DivideByZero, + RemainderNonPositive, + Unrepresentable(Value), + ArithOrCastOption, + TryUnwrapNone, + TryUnwrapConst, +} + +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvalError::DivideByZero => write!(f, "attempted division by zero"), + EvalError::RemainderNonPositive => write!(f, "remainder rhs must be positive"), + EvalError::Unrepresentable(value) => write!(f, "value `{value}` is unrepresentable"), + EvalError::ArithOrCastOption => { + write!(f, "arithmetic and casts on Value::Opt not supported") + } + EvalError::TryUnwrapNone => { + write!(f, "TryUnwrap called over expr evaluating to Opt(None)") + } + EvalError::TryUnwrapConst => write!( + f, + "TryUnwrap called over expr evaluating to Const (and not Opt)" + ), + } + } +} + +impl std::error::Error for EvalError {} + +impl Expr { + pub fn eval(&self) -> Result { + match self { + Expr::Const(typed_const) => Ok(Value::Const(typed_const.clone())), + Expr::BinOp(bin_op, lhs, rhs) => { + let lhs = lhs.eval()?; + let rhs = rhs.eval()?; + let BinOp { op, out_rep } = bin_op; + let (raw, rep0, rep1) = match (op, lhs, rhs) { + (BasicBinOp::Add, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 + rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Sub, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 - rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Mul, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 * rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Div, Value::Const(lhs), Value::Const(rhs)) => { + if rhs.0.is_zero() { + return Err(EvalError::DivideByZero); + } + (lhs.0 / rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Rem, Value::Const(lhs), Value::Const(rhs)) => { + if rhs.0.is_positive() { + (lhs.0 % rhs.0, lhs.1, rhs.1) + } else { + return Err(EvalError::RemainderNonPositive); + } + } + (_, Value::Opt(..), _) | (_, _, Value::Opt(..)) => { + return Err(EvalError::ArithOrCastOption) + } + }; + let rep_out = match out_rep { + Some(rep) => *rep, + None => { + if rep0 == rep1 { + rep0 + } else { + NumRep::Abstract + } + } + }; + Ok(Value::Const(TypedConst(raw, rep_out))) + } + Expr::UnaryOp(unary_op, expr) => { + let expr = expr.eval()?; + match (unary_op.op, expr) { + (BasicUnaryOp::Negate, Value::Const(TypedConst(n, rep))) => { + let rep_out = match unary_op.out_rep { + Some(rep) => rep, + None => rep, + }; + Ok(Value::Const(TypedConst(-n, rep_out))) + } + (BasicUnaryOp::AbsVal, Value::Const(TypedConst(n, rep))) => { + let rep_out = match unary_op.out_rep { + Some(rep) => rep, + None => rep, + }; + Ok(Value::Const(TypedConst(n.abs(), rep_out))) + } + (_, Value::Opt(_)) => return Err(EvalError::ArithOrCastOption), + } + } + Expr::Cast(num_rep, expr) => { + let val = expr.eval()?; + match val { + Value::Const(TypedConst(num, _rep)) => { + Ok(Value::Const(TypedConst(num, *num_rep))) + } + Value::Opt(_) => return Err(EvalError::ArithOrCastOption), + } + } + Expr::TryUnwrap(expr) => match expr.eval()? { + Value::Const(_) => return Err(EvalError::TryUnwrapConst), + Value::Opt(None) => return Err(EvalError::TryUnwrapNone), + Value::Opt(Some(x)) => Ok(*x), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn one_plus_one_is_two() -> Result<(), EvalError> { + let one = TypedConst(BigInt::one(), NumRep::Abstract); + let should_be_two = Expr::BinOp( + BinOp { + op: BasicBinOp::Add, + out_rep: None, + }, + Box::new(Expr::Const(one.clone())), + Box::new(Expr::Const(one)), + ); + assert_eq!( + should_be_two.eval()?.as_const().unwrap().as_raw_value(), + &BigInt::from(2) + ); + Ok(()) + } +} diff --git a/experiments/analytic-engine/src/main.rs b/experiments/analytic-engine/src/main.rs new file mode 100644 index 00000000..c09db116 --- /dev/null +++ b/experiments/analytic-engine/src/main.rs @@ -0,0 +1,5 @@ +use analytic_engine::*; + +fn main() { + println!("Hello, world!"); +} From 075ffb17444b39bd724a9a0e81770a68573e4928 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Tue, 29 Oct 2024 17:35:46 +1100 Subject: [PATCH 02/20] Add proptest tests (and dependency) to analytic-engine --- Cargo.lock | 36 ++------ experiments/analytic-engine/Cargo.toml | 1 + experiments/analytic-engine/src/lib.rs | 109 ++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d07cc786..e9195291 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,7 @@ dependencies = [ "bitflags 2.6.0", "num-bigint", "num-traits", + "proptest", ] [[package]] @@ -111,12 +112,6 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - [[package]] name = "cast" version = "0.3.0" @@ -653,20 +648,19 @@ dependencies = [ [[package]] name = "proptest" -version = "1.1.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", - "bitflags 1.3.2", - "byteorder", + "bit-vec", + "bitflags 2.6.0", "lazy_static", "num-traits", - "quick-error 2.0.1", "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.6.29", + "regex-syntax", "rusty-fork", "tempfile", "unarray", @@ -678,12 +672,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" version = "1.0.26" @@ -770,7 +758,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.4", + "regex-syntax", ] [[package]] @@ -781,15 +769,9 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.4" @@ -817,7 +799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error 1.2.3", + "quick-error", "tempfile", "wait-timeout", ] diff --git a/experiments/analytic-engine/Cargo.toml b/experiments/analytic-engine/Cargo.toml index 5250dfd5..556d99b4 100644 --- a/experiments/analytic-engine/Cargo.toml +++ b/experiments/analytic-engine/Cargo.toml @@ -11,3 +11,4 @@ bench = false num-bigint = "0.4" num-traits = "0.2" bitflags = "2.6" +proptest = "1.5.0" diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs index 74f7de73..ab96d021 100644 --- a/experiments/analytic-engine/src/lib.rs +++ b/experiments/analytic-engine/src/lib.rs @@ -1,3 +1,4 @@ +extern crate proptest; use num_bigint::BigInt; use num_traits::{One, Signed, Zero}; @@ -21,7 +22,7 @@ pub enum BasicUnaryOp { #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum NumRep { - Abstract, + Abstract { auto: bool }, Concrete { is_signed: bool, bit_width: BitWidth, @@ -62,11 +63,14 @@ impl NumRep { is_signed: false, bit_width: BitWidth::Bits64, }; + + pub const AUTO: NumRep = NumRep::Abstract { auto: true }; + pub const AMBIGUOUS: NumRep = NumRep::Abstract { auto: false }; } impl NumRep { pub const fn is_abstract(&self) -> bool { - matches!(self, NumRep::Abstract) + matches!(self, NumRep::Abstract { .. }) } } @@ -85,7 +89,7 @@ macro_rules! bounds_of { impl NumRep { fn as_bounds(&self) -> Option { let (min, max) = match self { - NumRep::Abstract => return None, + NumRep::Abstract { .. } => return None, &NumRep::U8 => bounds_of!(u8), &NumRep::U16 => bounds_of!(u16), &NumRep::U32 => bounds_of!(u32), @@ -97,6 +101,10 @@ impl NumRep { }; Some(Bounds { min, max }) } + + pub const fn is_auto(&self) -> bool { + matches!(self, NumRep::Abstract { auto: true }) + } } #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] @@ -122,12 +130,14 @@ impl std::fmt::Display for TypedConst { NumRep::I16 => write!(f, "{}i16", n), NumRep::I32 => write!(f, "{}i32", n), NumRep::I64 => write!(f, "{}i64", n), - NumRep::Abstract => write!(f, "{}??", n), + NumRep::AUTO => write!(f, "{}?", n), + NumRep::AMBIGUOUS => write!(f, "{}??", n), } } } impl TypedConst { + /// Returns `true` if the stored `NumRep` is abstract (either auto or ambiguous). pub fn is_abstract(&self) -> bool { self.1.is_abstract() } @@ -148,6 +158,25 @@ impl TypedConst { pub fn as_raw_value(&self) -> &BigInt { &self.0 } + + /// Type-agnostic equality on a pure mathematical level. + /// + /// Does not check for representablity of either value, nor even whether either representative is some flavor of `Abstract`. + pub fn eq_val(&self, other: &TypedConst) -> bool { + &self.0 == &other.0 + } + + /// Numeric equality test on `self`, that the value it holds is equal to `other` regardless of type. + /// + /// Saves the construction of a new TypedConst compared to [`eq_val`] if the query is made starting with a BigInt in mind. + pub fn eq_num(&self, other: &BigInt) -> bool { + &self.0 == other + } + + /// Returns the NumRep of a `TypedConst`. + pub fn get_rep(&self) -> NumRep { + self.1 + } } #[derive(Clone, PartialEq, Debug)] @@ -169,6 +198,7 @@ impl Value { } } + /// Extracts a reference to the `TypedConst` held within a Value, irrespective of its numeric representative. pub fn as_const(&self) -> Option<&TypedConst> { match self { Value::Const(c) => Some(c), @@ -190,7 +220,7 @@ impl std::fmt::Display for Value { #[derive(Clone, Copy, Debug)] pub struct BinOp { op: BasicBinOp, - // If None, picks either the common rep of the two arguments or Abstract if they disagree + // If None: op(T, T | auto) -> T, op(T0, T1) { T0 != T1 } -> ambiguous; otherwise, forces rep for `Some(rep)`` out_rep: Option, } @@ -280,10 +310,12 @@ impl Expr { let rep_out = match out_rep { Some(rep) => *rep, None => { - if rep0 == rep1 { + if rep0 == rep1 || rep1.is_auto() { rep0 + } else if rep0.is_auto() { + rep1 } else { - NumRep::Abstract + NumRep::AMBIGUOUS } } }; @@ -330,10 +362,39 @@ impl Expr { #[cfg(test)] mod tests { use super::*; + use proptest::prelude::*; + + + fn abstract_strategy() -> BoxedStrategy { + prop_oneof![ + Just(NumRep::AUTO), + Just(NumRep::AMBIGUOUS) + ].boxed() + } + + fn concrete_strategy() -> BoxedStrategy { + prop_oneof![ + Just(NumRep::U8), + Just(NumRep::U16), + Just(NumRep::U32), + Just(NumRep::U64), + Just(NumRep::I8), + Just(NumRep::I16), + Just(NumRep::I32), + Just(NumRep::I64), + ].boxed() + } + + fn numrep_strategy() -> BoxedStrategy { + prop_oneof![ + abstract_strategy(), + concrete_strategy(), + ].boxed() + } #[test] fn one_plus_one_is_two() -> Result<(), EvalError> { - let one = TypedConst(BigInt::one(), NumRep::Abstract); + let one = TypedConst(BigInt::one(), NumRep::AUTO); let should_be_two = Expr::BinOp( BinOp { op: BasicBinOp::Add, @@ -342,10 +403,34 @@ mod tests { Box::new(Expr::Const(one.clone())), Box::new(Expr::Const(one)), ); - assert_eq!( - should_be_two.eval()?.as_const().unwrap().as_raw_value(), - &BigInt::from(2) - ); + assert!(should_be_two.eval()?.as_const().unwrap().eq_num(&BigInt::from(2))); Ok(()) } + + proptest! { + #[test] + fn cast_works(orig in numrep_strategy(), tgt in numrep_strategy()) { + let one = TypedConst(BigInt::one(), orig); + let casted_one = Expr::Cast(tgt, Box::new(Expr::Const(one))); + let val = casted_one.eval().unwrap(); + let rep = val.as_const().unwrap().get_rep(); + prop_assert_eq!(rep, tgt); + } + + #[test] + fn auto_is_eagerly_erased(rep in numrep_strategy()) { + let one = TypedConst(BigInt::one(), NumRep::AUTO); + let rep_one = TypedConst(BigInt::one(), rep); + let two_should_be_rep = Expr::BinOp( + BinOp { + op: BasicBinOp::Add, + out_rep: None, + }, + Box::new(Expr::Const(one)), + Box::new(Expr::Const(rep_one)), + ); + let actual = two_should_be_rep.eval().unwrap().as_const().unwrap().get_rep(); + prop_assert_eq!(actual, rep); + } + } } From a593690f358980aecca0745c856b045412f370a3 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Wed, 30 Oct 2024 17:25:19 +1100 Subject: [PATCH 03/20] Add elaboration module for analytic-engine --- experiments/analytic-engine/src/core.rs | 515 ++++++++++++++ experiments/analytic-engine/src/elaborator.rs | 654 ++++++++++++++++++ experiments/analytic-engine/src/lib.rs | 438 +----------- 3 files changed, 1173 insertions(+), 434 deletions(-) create mode 100644 experiments/analytic-engine/src/core.rs create mode 100644 experiments/analytic-engine/src/elaborator.rs diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs new file mode 100644 index 00000000..03663bb3 --- /dev/null +++ b/experiments/analytic-engine/src/core.rs @@ -0,0 +1,515 @@ +use num_bigint::BigInt; +use num_traits::{One, Signed, Zero}; +use std::borrow::Cow; + +pub type Number = BigInt; + +/// Standalone ground operations on two numeric arguments +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BasicBinOp { + Add, + Sub, + Mul, + Div, + Rem, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BasicUnaryOp { + Negate, + AbsVal, +} + +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] +pub enum NumRep { + Abstract { + auto: bool, + }, + Concrete { + is_signed: bool, + bit_width: BitWidth, + }, +} + +impl NumRep { + pub const I8: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits8, + }; + pub const I16: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits16, + }; + pub const I32: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits32, + }; + pub const I64: NumRep = NumRep::Concrete { + is_signed: true, + bit_width: BitWidth::Bits64, + }; + + pub const U8: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits8, + }; + pub const U16: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits16, + }; + pub const U32: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits32, + }; + pub const U64: NumRep = NumRep::Concrete { + is_signed: false, + bit_width: BitWidth::Bits64, + }; + + pub const AUTO: NumRep = NumRep::Abstract { auto: true }; + pub const AMBIGUOUS: NumRep = NumRep::Abstract { auto: false }; +} + +impl NumRep { + pub const fn is_abstract(&self) -> bool { + matches!(self, NumRep::Abstract { .. }) + } +} + + +/// Representative min and max bounds for a numeric type +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct Bounds { + min: Number, + max: Number, +} + +impl std::fmt::Display for Bounds { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{},{}]", &self.min, &self.max) + } +} + +impl Bounds { + /// Returns `true` if every value in `sub_range` is also within `self`. + /// + /// If `inferior` has inverted bounds, will panic. + pub fn encompasses(&self, inferior: &Self) -> bool { + assert!(inferior.min <= inferior.max); + self.min <= inferior.min && self.max >= inferior.max + } + + /// Dual to [`encompasses`]. + /// + /// Returns `true` if every value in `self` is also within `superior`. + /// + /// If `superior` has inverted bounds, will panic. + pub fn is_encompassed_by(&self, superior: &Self) -> bool { + assert!(superior.min <= superior.max); + self.min >= superior.min && self.max <= superior.max + } + + pub(crate) fn unify<'a>(&'a self, bs2: &'a Bounds) -> Cow<'a, Bounds> { + if self.is_encompassed_by(bs2) { + Cow::Borrowed(bs2) + } else if self.encompasses(bs2) { + Cow::Borrowed(self) + } else { + Cow::Owned(Bounds { + min: Ord::min(&self.min, &bs2.min).clone(), + max: Ord::max(&self.max, &bs2.max).clone(), + }) + } + } +} + +macro_rules! bounds_of { + ( $t:ty ) => { + (Number::from(<$t>::MIN), Number::from(<$t>::MAX)) + }; +} + +impl NumRep { + pub(crate) fn as_bounds(&self) -> Option { + let (min, max) = match self { + NumRep::Abstract { .. } => return None, + &NumRep::U8 => bounds_of!(u8), + &NumRep::U16 => bounds_of!(u16), + &NumRep::U32 => bounds_of!(u32), + &NumRep::U64 => bounds_of!(u64), + &NumRep::I8 => bounds_of!(i8), + &NumRep::I16 => bounds_of!(i16), + &NumRep::I32 => bounds_of!(i32), + &NumRep::I64 => bounds_of!(i64), + }; + Some(Bounds { min, max }) + } + + pub const fn is_auto(&self) -> bool { + matches!(self, NumRep::Abstract { auto: true }) + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] +pub enum BitWidth { + Bits8, + Bits16, + Bits32, + Bits64, +} + +#[derive(Clone, PartialEq, Debug)] +pub struct TypedConst(BigInt, NumRep); + +impl std::fmt::Display for TypedConst { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let n = &self.0; + match self.1 { + NumRep::U8 => write!(f, "{}u8", n), + NumRep::U16 => write!(f, "{}u16", n), + NumRep::U32 => write!(f, "{}u32", n), + NumRep::U64 => write!(f, "{}u64", n), + NumRep::I8 => write!(f, "{}i8", n), + NumRep::I16 => write!(f, "{}i16", n), + NumRep::I32 => write!(f, "{}i32", n), + NumRep::I64 => write!(f, "{}i64", n), + NumRep::AUTO => write!(f, "{}?", n), + NumRep::AMBIGUOUS => write!(f, "{}??", n), + } + } +} + +impl TypedConst { + /// Returns `true` if the stored `NumRep` is abstract (either auto or ambiguous). + pub fn is_abstract(&self) -> bool { + self.1.is_abstract() + } + + /// Returns `true` if `self` is representable, which is true if either: + /// - The `NumRep` is `Abstract` + /// - The `NumRep` is concrete and `n` is in the bounds of the `NumRep` + pub fn is_representable(&self) -> bool { + let TypedConst(ref n, rep) = self; + if let Some(bounds) = rep.as_bounds() { + n >= &bounds.min && n <= &bounds.max + } else { + debug_assert!(rep.is_abstract()); + true + } + } + + pub fn as_raw_value(&self) -> &BigInt { + &self.0 + } + + /// Type-agnostic equality on a pure mathematical level. + /// + /// Does not check for representablity of either value, nor even whether either representative is some flavor of `Abstract`. + pub fn eq_val(&self, other: &TypedConst) -> bool { + &self.0 == &other.0 + } + + /// Numeric equality test on `self`, that the value it holds is equal to `other` regardless of type. + /// + /// Saves the construction of a new TypedConst compared to [`eq_val`] if the query is made starting with a BigInt in mind. + pub fn eq_num(&self, other: &BigInt) -> bool { + &self.0 == other + } + + /// Returns the NumRep of a `TypedConst`. + pub fn get_rep(&self) -> NumRep { + self.1 + } +} + +#[derive(Clone, PartialEq, Debug)] +pub enum Value { + Const(TypedConst), + // Opt(Option>), +} + +impl Value { + /// Returns `true` if `self` is representable: + /// - If `self` is a constant value, it must itself be representable + /// - If `self` is Some(x), `x` must be representable + /// + /// `None` is always representable. + pub fn is_representable(&self) -> bool { + match self { + Value::Const(c) => c.is_representable(), + // Value::Opt(value) => value.as_deref().map_or(true, Value::is_representable), + } + } + + /// Extracts a reference to the `TypedConst` held within a Value, irrespective of its numeric representative. + pub fn as_const(&self) -> Option<&TypedConst> { + match self { + Value::Const(c) => Some(c), + // Value::Opt(value) => value.as_deref().and_then(Value::as_const), + } + } +} + +impl std::fmt::Display for Value { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Const(num) => write!(f, "{num}"), + // Value::Opt(None) => write!(f, "None"), + // Value::Opt(Some(x)) => write!(f, "Some({})", x), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct BinOp { + op: BasicBinOp, + // If None: op(T, T | auto) -> T, op(T0, T1) { T0 != T1 } -> ambiguous; otherwise, forces rep for `Some(rep)`` + out_rep: Option, +} + +impl BinOp { + pub fn output_type(&self, left: NumRep, right: NumRep) -> NumRep { + if let Some(rep) = self.out_rep { + rep + } else if left == right || right.is_auto() { + left + } else if left.is_auto() { + right + } else { + NumRep::AMBIGUOUS + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct UnaryOp { + op: BasicUnaryOp, + // If None, will pick the same type as the input (even if this produces a temporary unrepresentable) + out_rep: Option, +} +impl UnaryOp { + fn output_type(&self, in_rep: NumRep) -> NumRep { + if let Some(rep) = self.out_rep { + rep + } else { + in_rep + } + } +} + +#[derive(Clone, Debug)] +pub enum Expr { + Const(TypedConst), + BinOp(BinOp, Box, Box), + UnaryOp(UnaryOp, Box), + Cast(NumRep, Box), + // TryUnwrap(Box), +} + +impl Expr { + pub(crate) fn get_rep(&self) -> NumRep { + match self { + Expr::Const(tc) => tc.get_rep(), + Expr::Cast(rep, _) => *rep, + Expr::BinOp(bin_op, expr, expr1) => { + bin_op.output_type(expr.get_rep(), expr1.get_rep()) + } + Expr::UnaryOp(unary_op, expr) => { + unary_op.output_type(expr.get_rep()) + } + } + } +} + +#[derive(Debug)] +pub enum EvalError { + DivideByZero, + RemainderNonPositive, + Unrepresentable(Value), + ArithOrCastOption, + // TryUnwrapNone, + // TryUnwrapConst, +} + +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvalError::DivideByZero => write!(f, "attempted division by zero"), + EvalError::RemainderNonPositive => write!(f, "remainder rhs must be positive"), + EvalError::Unrepresentable(value) => write!(f, "value `{value}` is unrepresentable"), + EvalError::ArithOrCastOption => { + write!(f, "arithmetic and casts on Value::Opt not supported") + } + // EvalError::TryUnwrapNone => { + // write!(f, "TryUnwrap called over expr evaluating to Opt(None)") + // } + // EvalError::TryUnwrapConst => write!( + // f, + // "TryUnwrap called over expr evaluating to Const (and not Opt)" + // ), + } + } +} + +impl std::error::Error for EvalError {} + +impl Expr { + pub fn eval(&self) -> Result { + match self { + Expr::Const(typed_const) => Ok(Value::Const(typed_const.clone())), + Expr::BinOp(bin_op, lhs, rhs) => { + let lhs = lhs.eval()?; + let rhs = rhs.eval()?; + let BinOp { op, out_rep } = bin_op; + let (raw, rep0, rep1) = match (op, lhs, rhs) { + (BasicBinOp::Add, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 + rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Sub, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 - rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Mul, Value::Const(lhs), Value::Const(rhs)) => { + (lhs.0 * rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Div, Value::Const(lhs), Value::Const(rhs)) => { + if rhs.0.is_zero() { + return Err(EvalError::DivideByZero); + } + (lhs.0 / rhs.0, lhs.1, rhs.1) + } + (BasicBinOp::Rem, Value::Const(lhs), Value::Const(rhs)) => { + if rhs.0.is_positive() { + (lhs.0 % rhs.0, lhs.1, rhs.1) + } else { + return Err(EvalError::RemainderNonPositive); + } + } + // (_, Value::Opt(..), _) | (_, _, Value::Opt(..)) => { + // return Err(EvalError::ArithOrCastOption) + // } + }; + let rep_out = match out_rep { + Some(rep) => *rep, + None => { + if rep0 == rep1 || rep1.is_auto() { + rep0 + } else if rep0.is_auto() { + rep1 + } else { + NumRep::AMBIGUOUS + } + } + }; + Ok(Value::Const(TypedConst(raw, rep_out))) + } + Expr::UnaryOp(unary_op, expr) => { + let expr = expr.eval()?; + match (unary_op.op, expr) { + (BasicUnaryOp::Negate, Value::Const(TypedConst(n, rep))) => { + let rep_out = match unary_op.out_rep { + Some(rep) => rep, + None => rep, + }; + Ok(Value::Const(TypedConst(-n, rep_out))) + } + (BasicUnaryOp::AbsVal, Value::Const(TypedConst(n, rep))) => { + let rep_out = match unary_op.out_rep { + Some(rep) => rep, + None => rep, + }; + Ok(Value::Const(TypedConst(n.abs(), rep_out))) + } + // (_, Value::Opt(_)) => return Err(EvalError::ArithOrCastOption), + } + } + Expr::Cast(num_rep, expr) => { + let val = expr.eval()?; + match val { + Value::Const(TypedConst(num, _rep)) => { + Ok(Value::Const(TypedConst(num, *num_rep))) + } + // Value::Opt(_) => return Err(EvalError::ArithOrCastOption), + } + } + // Expr::TryUnwrap(expr) => match expr.eval()? { + // Value::Const(_) => return Err(EvalError::TryUnwrapConst), + // Value::Opt(None) => return Err(EvalError::TryUnwrapNone), + // Value::Opt(Some(x)) => Ok(*x), + // }, + } + } +} + +#[cfg(test)] +mod tests { + use crate::core::*; + use proptest::prelude::*; + + fn abstract_strategy() -> BoxedStrategy { + prop_oneof![Just(NumRep::AUTO), Just(NumRep::AMBIGUOUS)].boxed() + } + + fn concrete_strategy() -> BoxedStrategy { + prop_oneof![ + Just(NumRep::U8), + Just(NumRep::U16), + Just(NumRep::U32), + Just(NumRep::U64), + Just(NumRep::I8), + Just(NumRep::I16), + Just(NumRep::I32), + Just(NumRep::I64), + ] + .boxed() + } + + fn numrep_strategy() -> BoxedStrategy { + prop_oneof![abstract_strategy(), concrete_strategy(),].boxed() + } + + #[test] + fn one_plus_one_is_two() -> Result<(), EvalError> { + let one = TypedConst(BigInt::one(), NumRep::AUTO); + let should_be_two = Expr::BinOp( + BinOp { + op: BasicBinOp::Add, + out_rep: None, + }, + Box::new(Expr::Const(one.clone())), + Box::new(Expr::Const(one)), + ); + assert!(should_be_two + .eval()? + .as_const() + .unwrap() + .eq_num(&BigInt::from(2))); + Ok(()) + } + + proptest! { + #[test] + fn cast_works(orig in numrep_strategy(), tgt in numrep_strategy()) { + let one = TypedConst(BigInt::one(), orig); + let casted_one = Expr::Cast(tgt, Box::new(Expr::Const(one))); + let val = casted_one.eval().unwrap(); + let rep = val.as_const().unwrap().get_rep(); + prop_assert_eq!(rep, tgt); + } + + #[test] + fn auto_is_eagerly_erased(rep in numrep_strategy()) { + let one = TypedConst(BigInt::one(), NumRep::AUTO); + let rep_one = TypedConst(BigInt::one(), rep); + let two_should_be_rep = Expr::BinOp( + BinOp { + op: BasicBinOp::Add, + out_rep: None, + }, + Box::new(Expr::Const(one)), + Box::new(Expr::Const(rep_one)), + ); + let actual = two_should_be_rep.eval().unwrap().as_const().unwrap().get_rep(); + prop_assert_eq!(actual, rep); + } + } +} diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs new file mode 100644 index 00000000..6c717ab3 --- /dev/null +++ b/experiments/analytic-engine/src/elaborator.rs @@ -0,0 +1,654 @@ +use crate::core::{Expr, NumRep, Value, TypedConst, UnaryOp, BinOp, BasicBinOp, BasicUnaryOp}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] +pub(crate) enum PrimInt { + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, +} + +pub const PRIM_INTS: [PrimInt; 8] = [PrimInt::U8, PrimInt::U16, PrimInt::U32, PrimInt::U64, PrimInt::I8, PrimInt::I16, PrimInt::I32, PrimInt::I64]; + +impl From for NumRep { + fn from(value: PrimInt) -> Self { + match value { + PrimInt::U8 => NumRep::U8, + PrimInt::U16 => NumRep::U16, + PrimInt::U32 => NumRep::U32, + PrimInt::U64 => NumRep::U64, + PrimInt::I8 => NumRep::I8, + PrimInt::I16 => NumRep::I16, + PrimInt::I32 => NumRep::I32, + PrimInt::I64 => NumRep::I64, + } + } +} + + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) enum IntType { + Prim(PrimInt), +} + +impl std::fmt::Display for IntType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + IntType::Prim(prim_int) => write!(f, "{:?}", prim_int), + } + } +} + + +#[derive(Clone, Debug)] +pub(crate) enum TypedExpr { + ElabConst(TypeRep, TypedConst), + ElabBinOp(TypeRep, TypedBinOp, Box>, Box>), + ElabUnaryOp(TypeRep, TypedUnaryOp, Box>), + ElabCast(TypeRep, NumRep, Box>), +} + +type Sig1 = (T, T); +type Sig2 = ((T, T), T); + +#[derive(Clone, Debug)] +struct TypedBinOp { + sig: Sig2, + inner: BinOp, +} + +#[derive(Clone, Debug)] +struct TypedUnaryOp { + sig: Sig1, + inner: UnaryOp, +} + +pub(crate) mod inference { + use std::collections::HashSet; + + use crate::core::{BitWidth, Bounds, Expr, NumRep, TypedConst}; + + use super::{IntType, PrimInt}; + + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + #[repr(transparent)] + pub struct UVar(usize); + + impl std::fmt::Display for UVar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "?{}", self.0) + } + } + + impl UVar { + pub fn new(ix: usize) -> Self { + Self(ix) + } + + pub fn to_usize(self) -> usize { + self.0 + } + } + + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] + pub enum UType { + Var(UVar), + Int(IntType), + } + + impl From for UType { + fn from(value: IntType) -> Self { + UType::Int(value) + } + } + + impl From for UType { + fn from(value: UVar) -> Self { + UType::Var(value) + } + } + + #[derive(Clone, Debug, Default)] + enum Alias { + #[default] + Ground, + BackRef(usize), + Canonical(HashSet), + } + + impl Alias { + pub const fn new() -> Alias { + Alias::Ground + } + + pub fn is_canonical_nonempty(&self) -> bool { + match self { + Alias::Canonical(x) => !x.is_empty(), + _ => false, + } + } + + pub fn as_backref(&self) -> Option { + match self { + Alias::Ground | Alias::Canonical(_) => None, + Alias::BackRef(ix) => Some(*ix), + } + } + + pub fn add_forward_ref(&mut self, tgt: usize) { + match self { + Alias::Ground => { + let _ = std::mem::replace(self, Alias::Canonical(HashSet::from([tgt]))); + } + Alias::BackRef(ix) => panic!("cannot add forward-ref to Alias::BackRef"), + Alias::Canonical(fwds) => { + fwds.insert(tgt); + } + } + } + + fn set_backref(&mut self, tgt: usize) -> Alias { + std::mem::replace(self, Alias::BackRef(tgt)) + } + + fn iter_fwd_refs<'a>(&'a self) -> Box + 'a> { + match self { + Alias::Ground | Alias::BackRef(_) => Box::new(std::iter::empty()), + Alias::Canonical(fwds) => Box::new(fwds.iter().copied()), + } + } + + fn contains_fwd_ref(&self, tgt: usize) -> bool { + match self { + Alias::Ground | Alias::BackRef(_) => false, + Alias::Canonical(fwds) => fwds.contains(&tgt), + } + } + } + + #[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] + pub enum Constraints { + #[default] + Indefinite, + Invariant(Constraint), + } + + impl Constraints { + pub fn new() -> Constraints { + Constraints::Indefinite + } + } + + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + pub enum Constraint { + Equiv(UType), + // Must be able to represent all values within the specified range + Encompasses(crate::core::Bounds), + } + + impl std::fmt::Display for Constraint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Constraint::Equiv(utype) => write!(f, "= {utype:?}"), + Constraint::Encompasses(bounds) => write!(f, "∈ {}", bounds), + } + } + } + + + impl From for Constraint { + fn from(value: UType) -> Self { + Constraint::Equiv(value) + } + } + + impl Constraint { + /// Speculatively checks if this constraint is definiitely satisfiable (as-is) by a given type-assignment. + /// + /// If this is not statically deterministic, returns `None`. + /// Returns `Some(true)` if the constraint is satisfiable by the assignment, and `Some(false)` otherwise. + pub(crate) fn is_satisfied_by(&self, candidate: IntType) -> Option { + match self { + Constraint::Equiv(utype) => match utype { + UType::Var(_) => None, + UType::Int(int_type) => Some(int_type == &candidate), + } + Constraint::Encompasses(bounds) => { + let IntType::Prim(candidate) = candidate; + Some(bounds.is_encompassed_by(&>::into(candidate).as_bounds().unwrap())) + } + } + } + + pub(crate) fn has_unique_assignment(&self) -> bool { + let mut solutions = 0; + for prim_int in super::PRIM_INTS.iter() { + match self.is_satisfied_by(IntType::Prim(*prim_int)) { + Some(true) => { + solutions += 1; + } + Some(false) => (), + None => return false, + } + } + solutions == 1 + } + } + + + #[derive(Debug)] + pub struct InferenceEngine { + constraints: Vec, + aliases: Vec, + } + + #[derive(Debug)] + enum InferenceError { + Unrepresentable(TypedConst, IntType), + BadUnification(Constraint, Constraint), + } + + impl std::fmt::Display for InferenceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + InferenceError::Unrepresentable(c, int_type) => write!(f, "inference requires that `{}` be assigned type `{}`, which cannot represent it", c, int_type), + InferenceError::BadUnification(cx1, cx2) => write!(f, "constraints `{}` and `{}` cannot be unified", cx1, cx2), + } + } + } + + impl std::error::Error for InferenceError {} + + pub type InferenceResult = Result; + + impl InferenceEngine { + pub fn new() -> Self { + Self { + constraints: Vec::new(), + aliases: Vec::new(), + } + } + + pub fn init_var_simple(&mut self, typ: UType) -> InferenceResult<(UVar, UType)> { + let newvar = self.get_new_uvar(); + let constr = Constraint::Equiv(typ); + self.unify_var_constraint(newvar, constr)?; + Ok((newvar, typ)) + } + + fn get_new_uvar(&mut self) -> UVar { + let ret = UVar(self.constraints.len()); + self.constraints.push(Constraints::new()); + self.aliases.push(Alias::new()); + ret + } + } + + impl InferenceEngine { + #[cfg_attr(not(test), allow(dead_code))] + pub fn check_uvar_sanity(&self) { + assert_eq!(self.constraints.len(), self.aliases.len()); + } + } + + impl InferenceEngine { + fn get_canonical_uvar(&self, v: UVar) -> UVar { + match self.aliases[v.0] { + Alias::Canonical(_) | Alias::Ground => v, + Alias::BackRef(ix) => UVar(ix), + } + } + + fn unify_var_constraint(&mut self, uvar: UVar, constraint: Constraint) -> InferenceResult { + let can_ix = self.get_canonical_uvar(uvar).0; + + match &self.constraints[can_ix] { + Constraints::Indefinite => { + let ret = constraint.clone(); + self.constraints[can_ix] = Constraints::Invariant(constraint); + Ok(ret) + } + Constraints::Invariant(prior) => { + let c1 = prior.clone(); + if c1 == constraint { + return Ok(c1); + } + let ret = self.unify_constraint_pair(c1, constraint)?; + self.constraints[can_ix] = Constraints::Invariant(ret.clone()); + Ok(ret) + } + } + } + + unsafe fn repoint(&mut self, lo: usize, hi: usize) { + self.aliases[hi].set_backref(lo); + self.aliases[lo].add_forward_ref(hi); + } + + unsafe fn transfer_constraints(&mut self, a1: usize, a2: usize) -> InferenceResult<&Constraints> { + if a1 == a2 { + return Ok(&self.constraints[a1]); + } + + match (&self.constraints[a1], &self.constraints[a2]) { + (Constraints::Indefinite, Constraints::Indefinite) => Ok(&Constraints::Indefinite), + (Constraints::Indefinite, _) => Ok(self.replace_constraints_from_index(a1, a2)), + (_, Constraints::Indefinite) => Ok(self.replace_constraints_from_index(a2, a1)), + (Constraints::Invariant(c1), Constraints::Invariant(c2)) => { + let c0 = self.unify_constraint_pair(c1.clone(), c2.clone())?; + let _ = self.replace_constraints_with_value(a1, Constraints::Invariant(c0.clone())); + let _ = self.replace_constraints_with_value(a2, Constraints::Invariant(c0)); + Ok(&self.constraints[a1]) + } + } + } + + #[must_use] + fn replace_constraints_from_index(&mut self, tgt_ix: usize, src_ix: usize) -> &Constraints { + assert_ne!(tgt_ix, src_ix); + let val = self.constraints[src_ix].clone(); + self.replace_constraints_with_value(tgt_ix, val) + } + + #[must_use] + fn replace_constraints_with_value(&mut self, ix: usize, val: Constraints) -> &Constraints { + self.constraints[ix] = val; + &self.constraints[ix] + } + + + fn unify_var_pair(&mut self, v1: UVar, v2: UVar) -> InferenceResult<&Constraints> { + if v1 == v2 { + return Ok(&self.constraints[v1.0]); + } + + // short-circuit if already equated + match (&self.aliases[v1.0], &self.aliases[v2.0]) { + (Alias::Ground, Alias::Ground) => { + if v1 < v2 { + unsafe { + self.repoint(v1.0, v2.0); + self.transfer_constraints(v1.0, v2.0) + } + } else { + unsafe { + self.repoint(v2.0, v1.0); + self.transfer_constraints(v2.0, v1.0) + } + } + } + (Alias::Ground, &Alias::BackRef(can_ix)) if v1.0 > can_ix => unsafe { + self.repoint(can_ix, v1.0); + self.transfer_constraints(can_ix, v1.0) + } + (Alias::Ground, &Alias::BackRef(can_ix)) if v1.0 < can_ix => { + debug_assert!( + self.aliases[can_ix].is_canonical_nonempty(), + "half-alias ?{can_ix}-|<-{v1}" + ); + debug_assert!( + !self.aliases[can_ix].contains_fwd_ref(v2.0), + "retrograde half-aliased 'forward' ref ?{can_ix}->|-{v2}" + ); + unsafe { self.recanonicalize(v1.0, can_ix) } + } + (Alias::Ground, &Alias::BackRef(_can_ix)) => { + unreachable!("unexpected half-alias {v1}-|<-{v2}"); + } + (&Alias::BackRef(can_ix), Alias::Ground) if v2.0 > can_ix => unsafe { + self.repoint(can_ix, v2.0); + self.transfer_constraints(can_ix, v2.0) + } + (&Alias::BackRef(can_ix), Alias::Ground) if v2.0 < can_ix => { + debug_assert!( + self.aliases[can_ix].is_canonical_nonempty(), + "half-alias ?{can_ix}-|<-{v1}" + ); + debug_assert!( + !self.aliases[can_ix].contains_fwd_ref(v2.0), + "retrograde half-aliased 'forward' ref ?{can_ix}->|-{v2}" + ); + unsafe { self.recanonicalize(v2.0, can_ix) } + } + (&Alias::BackRef(_can_ix), Alias::Ground) => { + unreachable!("unexpected half-alias {v2}-|<-{v1}"); + } + (Alias::Ground, Alias::Canonical(_)) => { + if v1.0 < v2.0 { + debug_assert!( + !self.aliases[v2.0].contains_fwd_ref(v1.0), + "retrograde half-aliased 'forward' ref {v2}->|-{v1}" + ); + unsafe { self.recanonicalize(v1.0, v2.0) } + } else { + unsafe { + self.repoint(v2.0, v1.0); + self.transfer_constraints(v2.0, v1.0) + } + } + } + (Alias::Canonical(_), Alias::Ground) => { + if v2.0 < v1.0 { + debug_assert!( + !self.aliases[v1.0].contains_fwd_ref(v2.0), + "retrograde half-aliased 'forward' ref {v1}->|-{v2}" + ); + unsafe { self.recanonicalize(v2.0, v1.0) } + } else { + unsafe { + self.repoint(v1.0, v2.0); + self.transfer_constraints(v1.0, v2.0) + } + } + } + (&Alias::BackRef(ix1), &Alias::BackRef(ix2)) if ix1 < ix2 => unsafe { + self.recanonicalize(ix1, ix2) + } + (&Alias::BackRef(ix1), &Alias::BackRef(ix2)) if ix2 < ix1 => unsafe { + self.recanonicalize(ix2, ix1) + } + (&Alias::BackRef(ix), &Alias::BackRef(_ix)) => { + // the two are equal so nothing needs to be changed; we will check both are forward-aliased, however + let common = &self.aliases[ix]; + debug_assert!( + common.contains_fwd_ref(v1.0), + "unexpected half-alias ?{ix}<-{v1}" + ); + debug_assert!( + common.contains_fwd_ref(v2.0), + "unexpected half-alias ?{ix}<-{v2}" + ); + Ok(&self.constraints[ix]) + } + (a1 @ Alias::BackRef(tgt), a2 @ Alias::Canonical(fwds)) => { + let left = fwds.contains(&v1.0); + let right = *tgt == v2.0; + + match (left, right) { + (true, true) => { + return Ok(&self.constraints[v2.0]); + } + (true, false) | (false, true) => { + unreachable!( + "mismatched back- and forward-references for {v1} ({a1:?}) and {v2} ({a2:?})" + ) + } + (false, false) => (), + } + + let ix1 = *tgt; + let ix2 = v2.0; + + // check not the actual indices, but the canonical indices for tie-breaking + if ix1 < ix2 { + unsafe { self.recanonicalize(ix1, ix2) } + } else { + unsafe { self.recanonicalize(ix2, ix1) } + } + } + (a1 @ Alias::Canonical(fwds), a2 @ Alias::BackRef(tgt)) => { + let left = fwds.contains(&v2.0); + let right = *tgt == v1.0; + + match (left, right) { + (true, true) => { + return Ok(&self.constraints[v1.0]); + } + (true, false) | (false, true) => { + unreachable!( + "mismatched forward- and back-references for {v1} ({a1:?}) and {v2} ({a2:?})" + ) + } + (false, false) => (), + } + + let ix1 = v1.0; + let ix2 = *tgt; + + + // check not the actual indices, but the canonical indices for tie-breaking + if ix1 < ix2 { + unsafe { self.recanonicalize(ix1, ix2) } + } else { + unsafe { self.recanonicalize(ix2, ix1) } + } + } + (Alias::Canonical(_), Alias::Canonical(_)) => { + if v1 < v2 { + unsafe { self.recanonicalize(v1.0, v2.0) } + } else { + unsafe { self.recanonicalize(v2.0, v1.0) } + } + } + } + } + + unsafe fn recanonicalize(&mut self, a1: usize, a2: usize) -> InferenceResult<&Constraints> { + let tmp = self.aliases[a2].set_backref(a1); + let iter = tmp.iter_fwd_refs(); + for a in iter { + assert!( + !self.aliases[a1].contains_fwd_ref(a), + "forward ref of ?{a2} is also a forward_ref of ?{a1}, somehow" + ); + self.repoint(a1, a); + } + self.aliases[a1].add_forward_ref(a2); + self.transfer_constraints(a1, a2) + } + + fn unify_utype(&mut self, left: UType, right: UType) -> InferenceResult { + match (left, right) { + (UType::Var(v1), UType::Var(v2)) => { + self.unify_var_pair(v1, v2)?; + Ok(UType::Var(Ord::min(v1, v2))) + } + (UType::Var(v), _) => { + let constraint = Constraint::Equiv(right); + let after = self.unify_var_constraint(v, constraint)?; + match after { + Constraint::Equiv(t) => Ok(t), + Constraint::Encompasses(_) => { + unreachable!("equiv should erase encompasses") + } + } + } + (_, UType::Var(v)) => { + let constraint = Constraint::Equiv(left); + let after = self.unify_var_constraint(v, constraint)?; + match after { + Constraint::Equiv(t) => Ok(t), + Constraint::Encompasses(_) => { + unreachable!("equiv should erase encompasses") + } + } + + } + (UType::Int(t0), UType::Int(t1)) => { + if t0 != t1 { + return Err(InferenceError::BadUnification(Constraint::Equiv(left), Constraint::Equiv(right))); + } + Ok(left) + } + } + } + + fn unify_utype_bounds(&mut self, utype: UType, bounds: &Bounds) -> InferenceResult { + match utype { + UType::Var(uvar) => { + self.unify_var_constraint(uvar, Constraint::Encompasses(bounds.clone())) + } + UType::Int(int_type) => { + let IntType::Prim(candidate) = int_type; + let soluble = bounds.is_encompassed_by(&>::into(candidate).as_bounds().unwrap()); + if soluble { + Ok(Constraint::Equiv(utype)) + } else { + Err(InferenceError::BadUnification(Constraint::Equiv(utype), Constraint::Encompasses(bounds.clone()))) + } + } + } + } + + fn unify_constraint_pair(&mut self, c1: Constraint, c2: Constraint) -> InferenceResult { + match (c1, c2) { + (Constraint::Equiv(t1), Constraint::Equiv(t2)) => { + if t1 == t2 { + Ok(Constraint::Equiv(t1)) + } else { + let t0 = self.unify_utype(t1, t2)?; + Ok(Constraint::Equiv(t0)) + } + } + (Constraint::Equiv(utype), Constraint::Encompasses(bounds)) + | (Constraint::Encompasses(bounds), Constraint::Equiv(utype)) => { + Ok(self.unify_utype_bounds(utype, &bounds)?) + } + (Constraint::Encompasses(bs1), Constraint::Encompasses(bs2)) => { + let bs0 = bs1.unify(&bs2).into_owned(); + Ok(Constraint::Encompasses(bs0)) + } + } + } + } + + impl InferenceEngine { + fn infer_var_expr(&mut self, e: &Expr) -> InferenceResult { + let topvar = match e { + Expr::Const(typed_const) => { + match typed_const.get_rep() { + NumRep::Abstract { .. } => self.get_new_uvar(), + NumRep::U8 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U8)))?.0, + NumRep::U16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U16)))?.0, + NumRep::U32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U32)))?.0, + NumRep::U64 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U64)))?.0, + NumRep::I8 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I8)))?.0, + NumRep::I16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I16)))?.0, + NumRep::I32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I32)))?.0, + NumRep::I64 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I64)))?.0, + } + } + Expr::BinOp(bin_op, lhs, rhs) => { + let var = self.get_new_uvar(); + let v_lhs = self.infer_var_expr(&lhs)?; + let v_rhs = self.infer_var_expr(&rhs)?; + let out_rep = bin_op.output_type(lhs.get_rep(), rhs.get_rep()); + match out_rep { + // FIXME - figure out what the logic should be... + _ => todo!(), + } + } + Expr::UnaryOp(unary_op, expr) => todo!(), + Expr::Cast(num_rep, expr) => todo!(), + }; + Ok(topvar) + } + } + + + +} diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs index ab96d021..540ae297 100644 --- a/experiments/analytic-engine/src/lib.rs +++ b/experiments/analytic-engine/src/lib.rs @@ -1,436 +1,6 @@ extern crate proptest; -use num_bigint::BigInt; -use num_traits::{One, Signed, Zero}; +extern crate num_bigint; +extern crate num_traits; -pub type Number = BigInt; - -/// Standalone ground operations on two numeric arguments -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum BasicBinOp { - Add, - Sub, - Mul, - Div, - Rem, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum BasicUnaryOp { - Negate, - AbsVal, -} - -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] -pub enum NumRep { - Abstract { auto: bool }, - Concrete { - is_signed: bool, - bit_width: BitWidth, - }, -} - -impl NumRep { - pub const I8: NumRep = NumRep::Concrete { - is_signed: true, - bit_width: BitWidth::Bits8, - }; - pub const I16: NumRep = NumRep::Concrete { - is_signed: true, - bit_width: BitWidth::Bits16, - }; - pub const I32: NumRep = NumRep::Concrete { - is_signed: true, - bit_width: BitWidth::Bits32, - }; - pub const I64: NumRep = NumRep::Concrete { - is_signed: true, - bit_width: BitWidth::Bits64, - }; - - pub const U8: NumRep = NumRep::Concrete { - is_signed: false, - bit_width: BitWidth::Bits8, - }; - pub const U16: NumRep = NumRep::Concrete { - is_signed: false, - bit_width: BitWidth::Bits16, - }; - pub const U32: NumRep = NumRep::Concrete { - is_signed: false, - bit_width: BitWidth::Bits32, - }; - pub const U64: NumRep = NumRep::Concrete { - is_signed: false, - bit_width: BitWidth::Bits64, - }; - - pub const AUTO: NumRep = NumRep::Abstract { auto: true }; - pub const AMBIGUOUS: NumRep = NumRep::Abstract { auto: false }; -} - -impl NumRep { - pub const fn is_abstract(&self) -> bool { - matches!(self, NumRep::Abstract { .. }) - } -} - -/// Representative min and max bounds for a numeric type -pub struct Bounds { - min: Number, - max: Number, -} - -macro_rules! bounds_of { - ( $t:ty ) => { - (Number::from(<$t>::MIN), Number::from(<$t>::MAX)) - }; -} - -impl NumRep { - fn as_bounds(&self) -> Option { - let (min, max) = match self { - NumRep::Abstract { .. } => return None, - &NumRep::U8 => bounds_of!(u8), - &NumRep::U16 => bounds_of!(u16), - &NumRep::U32 => bounds_of!(u32), - &NumRep::U64 => bounds_of!(u64), - &NumRep::I8 => bounds_of!(i8), - &NumRep::I16 => bounds_of!(i16), - &NumRep::I32 => bounds_of!(i32), - &NumRep::I64 => bounds_of!(i64), - }; - Some(Bounds { min, max }) - } - - pub const fn is_auto(&self) -> bool { - matches!(self, NumRep::Abstract { auto: true }) - } -} - -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] -pub enum BitWidth { - Bits8, - Bits16, - Bits32, - Bits64, -} - -#[derive(Clone, PartialEq, Debug)] -pub struct TypedConst(BigInt, NumRep); - -impl std::fmt::Display for TypedConst { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let n = &self.0; - match self.1 { - NumRep::U8 => write!(f, "{}u8", n), - NumRep::U16 => write!(f, "{}u16", n), - NumRep::U32 => write!(f, "{}u32", n), - NumRep::U64 => write!(f, "{}u64", n), - NumRep::I8 => write!(f, "{}i8", n), - NumRep::I16 => write!(f, "{}i16", n), - NumRep::I32 => write!(f, "{}i32", n), - NumRep::I64 => write!(f, "{}i64", n), - NumRep::AUTO => write!(f, "{}?", n), - NumRep::AMBIGUOUS => write!(f, "{}??", n), - } - } -} - -impl TypedConst { - /// Returns `true` if the stored `NumRep` is abstract (either auto or ambiguous). - pub fn is_abstract(&self) -> bool { - self.1.is_abstract() - } - - /// Returns `true` if `self` is representable, which is true if either: - /// - The `NumRep` is `Abstract` - /// - The `NumRep` is concrete and `n` is in the bounds of the `NumRep` - pub fn is_representable(&self) -> bool { - let TypedConst(ref n, rep) = self; - if let Some(bounds) = rep.as_bounds() { - n >= &bounds.min && n <= &bounds.max - } else { - debug_assert!(rep.is_abstract()); - true - } - } - - pub fn as_raw_value(&self) -> &BigInt { - &self.0 - } - - /// Type-agnostic equality on a pure mathematical level. - /// - /// Does not check for representablity of either value, nor even whether either representative is some flavor of `Abstract`. - pub fn eq_val(&self, other: &TypedConst) -> bool { - &self.0 == &other.0 - } - - /// Numeric equality test on `self`, that the value it holds is equal to `other` regardless of type. - /// - /// Saves the construction of a new TypedConst compared to [`eq_val`] if the query is made starting with a BigInt in mind. - pub fn eq_num(&self, other: &BigInt) -> bool { - &self.0 == other - } - - /// Returns the NumRep of a `TypedConst`. - pub fn get_rep(&self) -> NumRep { - self.1 - } -} - -#[derive(Clone, PartialEq, Debug)] -pub enum Value { - Const(TypedConst), - Opt(Option>), -} - -impl Value { - /// Returns `true` if `self` is representable: - /// - If `self` is a constant value, it must itself be representable - /// - If `self` is Some(x), `x` must be representable - /// - /// `None` is always representable. - pub fn is_representable(&self) -> bool { - match self { - Value::Const(c) => c.is_representable(), - Value::Opt(value) => value.as_deref().map_or(true, Value::is_representable), - } - } - - /// Extracts a reference to the `TypedConst` held within a Value, irrespective of its numeric representative. - pub fn as_const(&self) -> Option<&TypedConst> { - match self { - Value::Const(c) => Some(c), - Value::Opt(value) => value.as_deref().and_then(Value::as_const), - } - } -} - -impl std::fmt::Display for Value { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Value::Const(num) => write!(f, "{num}"), - Value::Opt(None) => write!(f, "None"), - Value::Opt(Some(x)) => write!(f, "Some({})", x), - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct BinOp { - op: BasicBinOp, - // If None: op(T, T | auto) -> T, op(T0, T1) { T0 != T1 } -> ambiguous; otherwise, forces rep for `Some(rep)`` - out_rep: Option, -} - -#[derive(Clone, Copy, Debug)] -pub struct UnaryOp { - op: BasicUnaryOp, - // If None, will pick the same type as the input (even if this produces a temporary unrepresentable) - out_rep: Option, -} - -#[derive(Clone, Debug)] -pub enum Expr { - Const(TypedConst), - BinOp(BinOp, Box, Box), - UnaryOp(UnaryOp, Box), - Cast(NumRep, Box), - TryUnwrap(Box), -} - -#[derive(Debug)] -pub enum EvalError { - DivideByZero, - RemainderNonPositive, - Unrepresentable(Value), - ArithOrCastOption, - TryUnwrapNone, - TryUnwrapConst, -} - -impl std::fmt::Display for EvalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EvalError::DivideByZero => write!(f, "attempted division by zero"), - EvalError::RemainderNonPositive => write!(f, "remainder rhs must be positive"), - EvalError::Unrepresentable(value) => write!(f, "value `{value}` is unrepresentable"), - EvalError::ArithOrCastOption => { - write!(f, "arithmetic and casts on Value::Opt not supported") - } - EvalError::TryUnwrapNone => { - write!(f, "TryUnwrap called over expr evaluating to Opt(None)") - } - EvalError::TryUnwrapConst => write!( - f, - "TryUnwrap called over expr evaluating to Const (and not Opt)" - ), - } - } -} - -impl std::error::Error for EvalError {} - -impl Expr { - pub fn eval(&self) -> Result { - match self { - Expr::Const(typed_const) => Ok(Value::Const(typed_const.clone())), - Expr::BinOp(bin_op, lhs, rhs) => { - let lhs = lhs.eval()?; - let rhs = rhs.eval()?; - let BinOp { op, out_rep } = bin_op; - let (raw, rep0, rep1) = match (op, lhs, rhs) { - (BasicBinOp::Add, Value::Const(lhs), Value::Const(rhs)) => { - (lhs.0 + rhs.0, lhs.1, rhs.1) - } - (BasicBinOp::Sub, Value::Const(lhs), Value::Const(rhs)) => { - (lhs.0 - rhs.0, lhs.1, rhs.1) - } - (BasicBinOp::Mul, Value::Const(lhs), Value::Const(rhs)) => { - (lhs.0 * rhs.0, lhs.1, rhs.1) - } - (BasicBinOp::Div, Value::Const(lhs), Value::Const(rhs)) => { - if rhs.0.is_zero() { - return Err(EvalError::DivideByZero); - } - (lhs.0 / rhs.0, lhs.1, rhs.1) - } - (BasicBinOp::Rem, Value::Const(lhs), Value::Const(rhs)) => { - if rhs.0.is_positive() { - (lhs.0 % rhs.0, lhs.1, rhs.1) - } else { - return Err(EvalError::RemainderNonPositive); - } - } - (_, Value::Opt(..), _) | (_, _, Value::Opt(..)) => { - return Err(EvalError::ArithOrCastOption) - } - }; - let rep_out = match out_rep { - Some(rep) => *rep, - None => { - if rep0 == rep1 || rep1.is_auto() { - rep0 - } else if rep0.is_auto() { - rep1 - } else { - NumRep::AMBIGUOUS - } - } - }; - Ok(Value::Const(TypedConst(raw, rep_out))) - } - Expr::UnaryOp(unary_op, expr) => { - let expr = expr.eval()?; - match (unary_op.op, expr) { - (BasicUnaryOp::Negate, Value::Const(TypedConst(n, rep))) => { - let rep_out = match unary_op.out_rep { - Some(rep) => rep, - None => rep, - }; - Ok(Value::Const(TypedConst(-n, rep_out))) - } - (BasicUnaryOp::AbsVal, Value::Const(TypedConst(n, rep))) => { - let rep_out = match unary_op.out_rep { - Some(rep) => rep, - None => rep, - }; - Ok(Value::Const(TypedConst(n.abs(), rep_out))) - } - (_, Value::Opt(_)) => return Err(EvalError::ArithOrCastOption), - } - } - Expr::Cast(num_rep, expr) => { - let val = expr.eval()?; - match val { - Value::Const(TypedConst(num, _rep)) => { - Ok(Value::Const(TypedConst(num, *num_rep))) - } - Value::Opt(_) => return Err(EvalError::ArithOrCastOption), - } - } - Expr::TryUnwrap(expr) => match expr.eval()? { - Value::Const(_) => return Err(EvalError::TryUnwrapConst), - Value::Opt(None) => return Err(EvalError::TryUnwrapNone), - Value::Opt(Some(x)) => Ok(*x), - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use proptest::prelude::*; - - - fn abstract_strategy() -> BoxedStrategy { - prop_oneof![ - Just(NumRep::AUTO), - Just(NumRep::AMBIGUOUS) - ].boxed() - } - - fn concrete_strategy() -> BoxedStrategy { - prop_oneof![ - Just(NumRep::U8), - Just(NumRep::U16), - Just(NumRep::U32), - Just(NumRep::U64), - Just(NumRep::I8), - Just(NumRep::I16), - Just(NumRep::I32), - Just(NumRep::I64), - ].boxed() - } - - fn numrep_strategy() -> BoxedStrategy { - prop_oneof![ - abstract_strategy(), - concrete_strategy(), - ].boxed() - } - - #[test] - fn one_plus_one_is_two() -> Result<(), EvalError> { - let one = TypedConst(BigInt::one(), NumRep::AUTO); - let should_be_two = Expr::BinOp( - BinOp { - op: BasicBinOp::Add, - out_rep: None, - }, - Box::new(Expr::Const(one.clone())), - Box::new(Expr::Const(one)), - ); - assert!(should_be_two.eval()?.as_const().unwrap().eq_num(&BigInt::from(2))); - Ok(()) - } - - proptest! { - #[test] - fn cast_works(orig in numrep_strategy(), tgt in numrep_strategy()) { - let one = TypedConst(BigInt::one(), orig); - let casted_one = Expr::Cast(tgt, Box::new(Expr::Const(one))); - let val = casted_one.eval().unwrap(); - let rep = val.as_const().unwrap().get_rep(); - prop_assert_eq!(rep, tgt); - } - - #[test] - fn auto_is_eagerly_erased(rep in numrep_strategy()) { - let one = TypedConst(BigInt::one(), NumRep::AUTO); - let rep_one = TypedConst(BigInt::one(), rep); - let two_should_be_rep = Expr::BinOp( - BinOp { - op: BasicBinOp::Add, - out_rep: None, - }, - Box::new(Expr::Const(one)), - Box::new(Expr::Const(rep_one)), - ); - let actual = two_should_be_rep.eval().unwrap().as_const().unwrap().get_rep(); - prop_assert_eq!(actual, rep); - } - } -} +pub mod core; +pub mod elaborator; From 7bac2a0a32428d024d7bd81b0aaded6d15ab8306 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Thu, 31 Oct 2024 19:34:35 +1100 Subject: [PATCH 04/20] Finish inference engine, elaborator, add printer --- experiments/analytic-engine/src/core.rs | 123 ++++-- experiments/analytic-engine/src/elaborator.rs | 409 +++++++++++++++++- experiments/analytic-engine/src/lib.rs | 1 + experiments/analytic-engine/src/printer.rs | 125 ++++++ 4 files changed, 587 insertions(+), 71 deletions(-) create mode 100644 experiments/analytic-engine/src/printer.rs diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index 03663bb3..f4c53c8e 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -22,15 +22,29 @@ pub enum BasicUnaryOp { #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] pub enum NumRep { - Abstract { - auto: bool, - }, + Auto, Concrete { is_signed: bool, bit_width: BitWidth, }, } +impl std::fmt::Display for NumRep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + NumRep::U8 => write!(f, "u8",), + NumRep::U16 => write!(f, "u16"), + NumRep::U32 => write!(f, "u32"), + NumRep::U64 => write!(f, "u64"), + NumRep::I8 => write!(f, "i8"), + NumRep::I16 => write!(f, "i16"), + NumRep::I32 => write!(f, "i32"), + NumRep::I64 => write!(f, "i64"), + NumRep::AUTO => write!(f, "?"), + } + } +} + impl NumRep { pub const I8: NumRep = NumRep::Concrete { is_signed: true, @@ -66,15 +80,10 @@ impl NumRep { bit_width: BitWidth::Bits64, }; - pub const AUTO: NumRep = NumRep::Abstract { auto: true }; - pub const AMBIGUOUS: NumRep = NumRep::Abstract { auto: false }; + pub const AUTO: NumRep = NumRep::Auto; } -impl NumRep { - pub const fn is_abstract(&self) -> bool { - matches!(self, NumRep::Abstract { .. }) - } -} + /// Representative min and max bounds for a numeric type @@ -91,6 +100,14 @@ impl std::fmt::Display for Bounds { } impl Bounds { + pub fn new(min: Number, max: Number) -> Self { + Self { min, max } + } + + pub fn singleton(n: Number) -> Self { + Self { min: n.clone(), max: n } + } + /// Returns `true` if every value in `sub_range` is also within `self`. /// /// If `inferior` has inverted bounds, will panic. @@ -132,7 +149,7 @@ macro_rules! bounds_of { impl NumRep { pub(crate) fn as_bounds(&self) -> Option { let (min, max) = match self { - NumRep::Abstract { .. } => return None, + NumRep::Auto => return None, &NumRep::U8 => bounds_of!(u8), &NumRep::U16 => bounds_of!(u16), &NumRep::U32 => bounds_of!(u32), @@ -145,8 +162,8 @@ impl NumRep { Some(Bounds { min, max }) } - pub const fn is_auto(&self) -> bool { - matches!(self, NumRep::Abstract { auto: true }) + pub const fn is_auto(self) -> bool { + matches!(self, NumRep::Auto) } } @@ -159,7 +176,7 @@ pub enum BitWidth { } #[derive(Clone, PartialEq, Debug)] -pub struct TypedConst(BigInt, NumRep); +pub struct TypedConst(pub BigInt, pub NumRep); impl std::fmt::Display for TypedConst { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -174,15 +191,14 @@ impl std::fmt::Display for TypedConst { NumRep::I32 => write!(f, "{}i32", n), NumRep::I64 => write!(f, "{}i64", n), NumRep::AUTO => write!(f, "{}?", n), - NumRep::AMBIGUOUS => write!(f, "{}??", n), } } } impl TypedConst { /// Returns `true` if the stored `NumRep` is abstract (either auto or ambiguous). - pub fn is_abstract(&self) -> bool { - self.1.is_abstract() + pub fn is_abstract(self) -> bool { + self.1.is_auto() } /// Returns `true` if `self` is representable, which is true if either: @@ -193,7 +209,7 @@ impl TypedConst { if let Some(bounds) = rep.as_bounds() { n >= &bounds.min && n <= &bounds.max } else { - debug_assert!(rep.is_abstract()); + debug_assert!(rep.is_auto()); true } } @@ -262,23 +278,35 @@ impl std::fmt::Display for Value { #[derive(Clone, Copy, Debug)] pub struct BinOp { - op: BasicBinOp, + pub op: BasicBinOp, // If None: op(T, T | auto) -> T, op(T0, T1) { T0 != T1 } -> ambiguous; otherwise, forces rep for `Some(rep)`` - out_rep: Option, + pub out_rep: Option, } impl BinOp { - pub fn output_type(&self, left: NumRep, right: NumRep) -> NumRep { + pub fn output_type(&self, left: NumRep, right: NumRep) -> Option { if let Some(rep) = self.out_rep { - rep + Some(rep) } else if left == right || right.is_auto() { - left + Some(left) } else if left.is_auto() { - right + Some(right) } else { - NumRep::AMBIGUOUS + None } } + + pub fn cast_rep(&self) -> Option { + self.out_rep + } + + pub fn is_cast_and(&self, predicate: impl Fn(NumRep) -> bool) -> bool { + self.out_rep.is_some_and(predicate) + } + + pub(crate) fn get_op(&self) -> BasicBinOp { + self.op + } } #[derive(Clone, Copy, Debug)] @@ -289,12 +317,24 @@ pub struct UnaryOp { } impl UnaryOp { fn output_type(&self, in_rep: NumRep) -> NumRep { - if let Some(rep) = self.out_rep { + if let Some(rep) = self.out_rep { rep } else { in_rep } } + + pub fn cast_rep(&self) -> Option { + self.out_rep + } + + pub fn is_cast_and(&self, predicate: fn(NumRep) -> bool) -> bool { + self.out_rep.is_some_and(predicate) + } + + pub(crate) fn get_op(&self) -> BasicUnaryOp { + self.op + } } #[derive(Clone, Debug)] @@ -307,15 +347,15 @@ pub enum Expr { } impl Expr { - pub(crate) fn get_rep(&self) -> NumRep { + pub(crate) fn get_rep(&self) -> Option { match self { - Expr::Const(tc) => tc.get_rep(), - Expr::Cast(rep, _) => *rep, + Expr::Const(tc) => Some(tc.get_rep()), + Expr::Cast(rep, _) => Some(*rep), Expr::BinOp(bin_op, expr, expr1) => { - bin_op.output_type(expr.get_rep(), expr1.get_rep()) + bin_op.output_type(expr.get_rep()?, expr1.get_rep()?) } Expr::UnaryOp(unary_op, expr) => { - unary_op.output_type(expr.get_rep()) + Some(unary_op.output_type(expr.get_rep()?)) } } } @@ -325,10 +365,7 @@ impl Expr { pub enum EvalError { DivideByZero, RemainderNonPositive, - Unrepresentable(Value), - ArithOrCastOption, - // TryUnwrapNone, - // TryUnwrapConst, + Ambiguous(NumRep, NumRep), } impl std::fmt::Display for EvalError { @@ -336,17 +373,9 @@ impl std::fmt::Display for EvalError { match self { EvalError::DivideByZero => write!(f, "attempted division by zero"), EvalError::RemainderNonPositive => write!(f, "remainder rhs must be positive"), - EvalError::Unrepresentable(value) => write!(f, "value `{value}` is unrepresentable"), - EvalError::ArithOrCastOption => { - write!(f, "arithmetic and casts on Value::Opt not supported") + EvalError::Ambiguous(rep0, rep1) => { + write!(f, "operation over {rep0} and {rep1} must have an explicit output representation to be evaluated") } - // EvalError::TryUnwrapNone => { - // write!(f, "TryUnwrap called over expr evaluating to Opt(None)") - // } - // EvalError::TryUnwrapConst => write!( - // f, - // "TryUnwrap called over expr evaluating to Const (and not Opt)" - // ), } } } @@ -396,7 +425,7 @@ impl Expr { } else if rep0.is_auto() { rep1 } else { - NumRep::AMBIGUOUS + return Err(EvalError::Ambiguous(rep0, rep1)) } } }; @@ -446,7 +475,7 @@ mod tests { use proptest::prelude::*; fn abstract_strategy() -> BoxedStrategy { - prop_oneof![Just(NumRep::AUTO), Just(NumRep::AMBIGUOUS)].boxed() + prop_oneof![Just(NumRep::AUTO)].boxed() } fn concrete_strategy() -> BoxedStrategy { diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs index 6c717ab3..6eccda8f 100644 --- a/experiments/analytic-engine/src/elaborator.rs +++ b/experiments/analytic-engine/src/elaborator.rs @@ -14,6 +14,35 @@ pub(crate) enum PrimInt { pub const PRIM_INTS: [PrimInt; 8] = [PrimInt::U8, PrimInt::U16, PrimInt::U32, PrimInt::U64, PrimInt::I8, PrimInt::I16, PrimInt::I32, PrimInt::I64]; +#[derive(Debug)] +pub struct TryFromAutoError; + +impl std::fmt::Display for TryFromAutoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "cannot convert `NumRep::AUTO` to `PrimInt`") + } +} + +impl std::error::Error for TryFromAutoError {} + +impl TryFrom for PrimInt { + type Error = TryFromAutoError; + + fn try_from(value: NumRep) -> Result { + match value { + NumRep::Auto => Err(TryFromAutoError), + NumRep::U8 => Ok(PrimInt::U8), + NumRep::U16 => Ok(PrimInt::U16), + NumRep::U32 => Ok(PrimInt::U32), + NumRep::U64 => Ok(PrimInt::U64), + NumRep::I8 => Ok(PrimInt::I8), + NumRep::I16 => Ok(PrimInt::I16), + NumRep::I32 => Ok(PrimInt::I32), + NumRep::I64 => Ok(PrimInt::I64), + } + } +} + impl From for NumRep { fn from(value: PrimInt) -> Self { match value { @@ -52,25 +81,37 @@ pub(crate) enum TypedExpr { ElabCast(TypeRep, NumRep, Box>), } +impl TypedExpr { + pub fn get_type(&self) -> &T { + match self { + TypedExpr::ElabConst(t, _) => t, + TypedExpr::ElabBinOp(t, _, _, _) => t, + TypedExpr::ElabUnaryOp(t, _, _) => t, + TypedExpr::ElabCast(t, _, _) => t, + } + } + +} + type Sig1 = (T, T); type Sig2 = ((T, T), T); #[derive(Clone, Debug)] -struct TypedBinOp { - sig: Sig2, - inner: BinOp, +pub(crate) struct TypedBinOp { + pub(crate) sig: Sig2, + pub(crate) inner: BinOp, } #[derive(Clone, Debug)] -struct TypedUnaryOp { - sig: Sig1, - inner: UnaryOp, +pub(crate) struct TypedUnaryOp { + pub(crate) sig: Sig1, + pub(crate) inner: UnaryOp, } pub(crate) mod inference { use std::collections::HashSet; - use crate::core::{BitWidth, Bounds, Expr, NumRep, TypedConst}; + use crate::core::{Bounds, Expr, NumRep, TypedConst}; use super::{IntType, PrimInt}; @@ -112,6 +153,13 @@ pub(crate) mod inference { } } + #[derive(Clone, Debug, PartialEq, Eq)] + pub(crate) enum VType { + Int(IntType), + Within(Bounds), + Abstract(UType), + } + #[derive(Clone, Debug, Default)] enum Alias { #[default] @@ -190,6 +238,7 @@ pub(crate) mod inference { Encompasses(crate::core::Bounds), } + impl std::fmt::Display for Constraint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -225,6 +274,7 @@ pub(crate) mod inference { } pub(crate) fn has_unique_assignment(&self) -> bool { + // REVIEW - there are smarter ways of calculating this let mut solutions = 0; for prim_int in super::PRIM_INTS.iter() { match self.is_satisfied_by(IntType::Prim(*prim_int)) { @@ -237,6 +287,26 @@ pub(crate) mod inference { } solutions == 1 } + + // NOTE - should only be called on Encompasses + pub(crate) fn get_unique_solution(&self) -> InferenceResult { + // REVIEW - there are smarter ways of calculating this + let mut solutions = Vec::with_capacity(8); + for prim_int in super::PRIM_INTS.iter() { + match self.is_satisfied_by(IntType::Prim(*prim_int)) { + Some(true) => { + solutions.push(*prim_int); + } + Some(false) => (), + None => panic!("unexpected call to get_unique_solution on `{self}` (either trivial or insoluble)"), + } + } + match solutions.as_slice() { + [] => Err(InferenceError::NoSolution), + [uniq] => Ok(IntType::Prim(*uniq)), + _ => Err(InferenceError::MultipleSolutions), + } + } } @@ -247,9 +317,14 @@ pub(crate) mod inference { } #[derive(Debug)] - enum InferenceError { + pub enum InferenceError { Unrepresentable(TypedConst, IntType), BadUnification(Constraint, Constraint), + AbstractCast, + Ambiguous, + NoSolution, + MultipleSolutions, + Eval(crate::core::EvalError), } impl std::fmt::Display for InferenceError { @@ -257,11 +332,23 @@ pub(crate) mod inference { match self { InferenceError::Unrepresentable(c, int_type) => write!(f, "inference requires that `{}` be assigned type `{}`, which cannot represent it", c, int_type), InferenceError::BadUnification(cx1, cx2) => write!(f, "constraints `{}` and `{}` cannot be unified", cx1, cx2), + InferenceError::AbstractCast => write!(f, "casts and operations cannot explicitly produce abstract NumReps"), + InferenceError::Ambiguous => write!(f, "mixed-type binary operation must have out_rep on operation to avoid ambiguity"), + InferenceError::Eval(e) => write!(f, "inference abandoned due to evaluation error: {}", e), + InferenceError::NoSolution => write!(f, "no valid assignment of PrimInt types produce a fully representable tree"), + InferenceError::MultipleSolutions => write!(f, "multiple assignments of PrimInt produce a fully representable tree, in absence of tie-breaking mechanism"), } } } - impl std::error::Error for InferenceError {} + impl std::error::Error for InferenceError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + InferenceError::Eval(e) => Some(e), + _ => None, + } + } + } pub type InferenceResult = Result; @@ -594,6 +681,20 @@ pub(crate) mod inference { } } + fn unify_var_utype(&mut self, uvar: UVar, utype: UType) -> InferenceResult<()> { + let _ = self.unify_var_constraint(uvar, Constraint::Equiv(utype))?; + Ok(()) + } + + + fn unify_var_rep(&mut self, uvar: UVar, rep: NumRep) -> InferenceResult<()> { + if rep.is_auto() { + return Ok(()); + } + let t = UType::Int(IntType::Prim(PrimInt::try_from(rep).unwrap())); + self.unify_var_utype(uvar, t) + } + fn unify_constraint_pair(&mut self, c1: Constraint, c2: Constraint) -> InferenceResult { match (c1, c2) { (Constraint::Equiv(t1), Constraint::Equiv(t2)) => { @@ -617,11 +718,16 @@ pub(crate) mod inference { } impl InferenceEngine { - fn infer_var_expr(&mut self, e: &Expr) -> InferenceResult { - let topvar = match e { + pub(crate) fn infer_var_expr(&mut self, e: &Expr) -> InferenceResult<(UVar, NumRep)> { + let (top_var, top_rep) = match e { Expr::Const(typed_const) => { - match typed_const.get_rep() { - NumRep::Abstract { .. } => self.get_new_uvar(), + let rep = typed_const.get_rep(); + let var = match rep { + NumRep::AUTO => { + let this_var = self.get_new_uvar(); + self.unify_var_constraint(this_var, Constraint::Encompasses(Bounds::singleton(typed_const.as_raw_value().clone())))?; + this_var + } NumRep::U8 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U8)))?.0, NumRep::U16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U16)))?.0, NumRep::U32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U32)))?.0, @@ -630,25 +736,280 @@ pub(crate) mod inference { NumRep::I16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I16)))?.0, NumRep::I32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I32)))?.0, NumRep::I64 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I64)))?.0, - } + }; + (var, rep) } Expr::BinOp(bin_op, lhs, rhs) => { - let var = self.get_new_uvar(); - let v_lhs = self.infer_var_expr(&lhs)?; - let v_rhs = self.infer_var_expr(&rhs)?; - let out_rep = bin_op.output_type(lhs.get_rep(), rhs.get_rep()); - match out_rep { - // FIXME - figure out what the logic should be... - _ => todo!(), + let this_var = self.get_new_uvar(); + let (l_var, l_rep) = self.infer_var_expr(&lhs)?; + let (r_var, r_rep) = self.infer_var_expr(&rhs)?; + if bin_op.is_cast_and(NumRep::is_auto) { + return Err(InferenceError::AbstractCast); } + let cast_rep = bin_op.cast_rep(); + let this_rep = match (l_rep, r_rep) { + (NumRep::AUTO, NumRep::AUTO) => { + self.unify_var_pair(this_var, l_var)?; + self.unify_var_pair(this_var, r_var)?; + if let Some(rep) = cast_rep { + self.unify_var_rep(this_var, rep)?; + rep + } else { + { + // REVIEW - do we need to go this far? + match e.eval() { + Ok(v) => match v.as_const() { + Some(c) => { + // NOTE - if there is a const-evaluable result for the computation, use it to refine our constraints on which types satisfy the aliased Auto + let bounds = Bounds::singleton(c.as_raw_value().clone()); + self.unify_var_constraint(this_var, Constraint::Encompasses(bounds))?; + } + None => { + // FIXME - this isn't a hard error necessarily, but our model isn't complex enough for non-TypedConst values to emerge + unimplemented!("Value::AsConst returned None (unexpectedly) when called from InferenceEngine::infer_var_expr"); + } + } + Err(e) => { + // NOTE - If the computation will fail regardless, there is no need to infer the type-information of the AST + // REVIEW - make sure that we are confident in EvalErrors being sound reasons to fail type-inference, both now and going forward + return Err(InferenceError::Eval(e)) + } + } + } + NumRep::AUTO + } + } + (rep0, rep1) if rep0 == rep1 => { + if let Some(rep) = cast_rep { + self.unify_var_rep(this_var, rep)?; + rep + } else { + self.unify_var_rep(this_var, rep0)?; + rep0 + } + } + (rep0, rep1) => { + if let Some(rep) = cast_rep { + self.unify_var_rep(this_var, rep)?; + if l_rep.is_auto() { + debug_assert!(!r_rep.is_auto()); + self.unify_var_pair(this_var, l_var)?; + } + if r_rep.is_auto() { + debug_assert!(!l_rep.is_auto()); + self.unify_var_pair(this_var, r_var)?; + } + rep + } else { + if l_rep.is_auto() { + debug_assert!(!r_rep.is_auto()); + self.unify_var_rep(this_var, rep1)?; + self.unify_var_rep(l_var, rep1)?; + rep1 + } else if r_rep.is_auto() { + self.unify_var_rep(this_var, rep0)?; + self.unify_var_rep(r_var, rep0)?; + rep0 + } else { + return Err(InferenceError::Ambiguous); + } + } + } + + }; + (this_var, this_rep) + } + Expr::UnaryOp(unary_op, expr) => { + let this_var = self.get_new_uvar(); + let (inner_var, inner_rep) = self.infer_var_expr(&expr)?; + if unary_op.is_cast_and(NumRep::is_auto) { + return Err(InferenceError::AbstractCast); + } + let cast_rep = unary_op.cast_rep(); + let this_rep = match inner_rep { + NumRep::AUTO => { + self.unify_var_pair(this_var, inner_var)?; + if let Some(rep) = cast_rep { + self.unify_var_rep(this_var, rep)?; + rep + } else { + { + // REVIEW - do we need to go this far? + match e.eval() { + Ok(v) => match v.as_const() { + Some(c) => { + // NOTE - if there is a const-evaluable result for the computation, use it to refine our constraints on which types satisfy the aliased Auto + let bounds = Bounds::singleton(c.as_raw_value().clone()); + self.unify_var_constraint(this_var, Constraint::Encompasses(bounds))?; + } + None => { + // FIXME - this isn't a hard error necessarily, but our model isn't complex enough for non-TypedConst values to emerge + unimplemented!("Value::AsConst returned None (unexpectedly) when called from InferenceEngine::infer_var_expr"); + } + } + Err(e) => { + // NOTE - If the computation will fail regardless, there is no need to infer the type-information of the AST + // REVIEW - make sure that we are confident in EvalErrors being sound reasons to fail type-inference, both now and going forward + return Err(InferenceError::Eval(e)) + } + } + } + NumRep::AUTO + } + } + rep0 => { + if let Some(rep) = cast_rep { + self.unify_var_rep(this_var, rep)?; + rep + } else { + self.unify_var_rep(this_var, rep0)?; + rep0 + } + } + }; + (this_var, this_rep) + } + Expr::Cast(rep, expr) => { + let this_var = self.get_new_uvar(); + let (inner_var, inner_rep) = self.infer_var_expr(&expr)?; + if rep.is_auto() { + return Err(InferenceError::AbstractCast); + } + if inner_rep.is_auto() { + self.unify_var_rep(inner_var, *rep)?; + } + self.unify_var_rep(this_var, *rep)?; + (this_var, *rep) } - Expr::UnaryOp(unary_op, expr) => todo!(), - Expr::Cast(num_rep, expr) => todo!(), }; - Ok(topvar) + Ok((top_var, top_rep)) + } + + fn to_whnf_vtype(&self, t: UType) -> VType { + match t { + UType::Var(v) => { + let v0 = self.get_canonical_uvar(v); + match &self.constraints[v0.0] { + Constraints::Indefinite => VType::Abstract(v0.into()), + Constraints::Invariant(Constraint::Equiv(ut)) => self.to_whnf_vtype(*ut), + Constraints::Invariant(Constraint::Encompasses(bounds)) => VType::Within(bounds.clone()), + } + } + UType::Int(int_type) => VType::Int(int_type), + } + } + + pub(crate) fn substitute_uvar_vtype(&self, v: UVar) -> InferenceResult> { + match &self.constraints[self.get_canonical_uvar(v).0] { + Constraints::Indefinite => Ok(None), + Constraints::Invariant(cx) => Ok(match cx { + Constraint::Equiv(ut) => Some(self.to_whnf_vtype(*ut)), + Constraint::Encompasses(bounds) => Some(VType::Within(bounds.clone())) + }) + } + } + } + + impl InferenceEngine { + pub fn reify(&self, t: UType) -> Option { + match t { + UType::Var(uv) => { + let v = self.get_canonical_uvar(uv); + match self.substitute_uvar_vtype(v) { + Ok(Some(t0)) => match t0 { + VType::Int(int_type) => Some(int_type), + VType::Within(bounds) => match Constraint::get_unique_solution(&Constraint::Encompasses(bounds.clone())) { + Ok(int_type) => Some(int_type), + Err(_) => None, + } + VType::Abstract(utype) => self.reify(utype), + } + Err(_) => None, + Ok(None) => { + match &self.constraints[v.0] { + _ => None, + } + } + } + } + UType::Int(i) => Some(i), + } } } +} + +use inference::{InferenceEngine, UVar}; + +pub struct Elaborator { + next_index: usize, + ie: InferenceEngine, +} + +impl Elaborator { + fn get_and_increment_index(&mut self) -> usize { + let ret = self.next_index; + self.next_index += 1; + ret + } + + fn increment_index(&mut self) { + self.next_index += 1; + } + + fn get_index(&self) -> usize { + self.next_index + } + pub(crate) fn new(ie: InferenceEngine) -> Self { + Self { next_index: 0, ie } + } + fn get_type_from_index(&self, index: usize) -> IntType { + let uvar = UVar::new(index); + let Some(t) = self.ie.reify(uvar.into()) else { + unreachable!("unable to reify {uvar}") + }; + t + } + pub(crate) fn elaborate_expr(&mut self, expr: &Expr) -> TypedExpr { + let index = self.get_and_increment_index(); + match expr { + Expr::Const(typed_const) => { + let t = self.get_type_from_index(index); + TypedExpr::ElabConst(t, typed_const.clone()) + } + Expr::BinOp(bin_op, x, y) => { + let t_x = self.elaborate_expr(x); + let t_y = self.elaborate_expr(y); + let t = self.get_type_from_index(index); + TypedExpr::ElabBinOp( + t, + TypedBinOp { + sig: ((*t_x.get_type(), *t_y.get_type()), t), + inner: *bin_op, + }, + Box::new(t_x), + Box::new(t_y), + ) + } + Expr::UnaryOp(unary_op, inner) => { + let t_inner = self.elaborate_expr(inner); + let t = self.get_type_from_index(index); + TypedExpr::ElabUnaryOp( + t, + TypedUnaryOp { + sig: (*t_inner.get_type(), t), + inner: *unary_op, + }, + Box::new(t_inner), + ) + } + Expr::Cast(rep, inner) => { + let t_inner = self.elaborate_expr(inner); + let t = self.get_type_from_index(index); + TypedExpr::ElabCast(t, *rep, Box::new(t_inner)) + } + } + } } diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs index 540ae297..14972267 100644 --- a/experiments/analytic-engine/src/lib.rs +++ b/experiments/analytic-engine/src/lib.rs @@ -4,3 +4,4 @@ extern crate num_traits; pub mod core; pub mod elaborator; +pub mod printer; diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs new file mode 100644 index 00000000..52c15332 --- /dev/null +++ b/experiments/analytic-engine/src/printer.rs @@ -0,0 +1,125 @@ +use crate::elaborator::{TypedExpr, TypedBinOp, TypedUnaryOp, IntType, PrimInt, Elaborator}; +use num_bigint::BigInt; +use crate::core::{BasicBinOp, BinOp, Expr, NumRep, UnaryOp}; +use crate::elaborator::inference::InferenceEngine; + +fn show_bin_op(bin_op: &BinOp) -> String { + let token = match bin_op.get_op() { + BasicBinOp::Add => "+", + BasicBinOp::Sub => "-", + BasicBinOp::Mul => "*", + BasicBinOp::Div => "/", + BasicBinOp::Rem => "%", + }; + if let Some(rep) = bin_op.cast_rep() { + format!("{}{}", token, rep) + } else { + format!("{}", token) + } +} + +fn show_typed_bin_op(bin_op: &TypedBinOp) -> String { + let token = match bin_op.inner.get_op() { + BasicBinOp::Add => "+", + BasicBinOp::Sub => "-", + BasicBinOp::Mul => "*", + BasicBinOp::Div => "/", + BasicBinOp::Rem => "%", + }; + format!("({} : ({},{}) -> {})", token, bin_op.sig.0.0, bin_op.sig.0.1, bin_op.sig.1) +} + +fn show_typed_unary_op(unary_op: &TypedUnaryOp) -> String { + let token = match unary_op.inner.get_op() { + crate::core::BasicUnaryOp::Negate => "~", + crate::core::BasicUnaryOp::AbsVal => "abs", + }; + format!("({} : {} -> {})", token, unary_op.sig.1, unary_op.sig.1) +} + + +fn show_unary_op(unary_op: &UnaryOp) -> String { + let token = match unary_op.get_op() { + crate::core::BasicUnaryOp::Negate => "~", + crate::core::BasicUnaryOp::AbsVal => "abs", + }; + if let Some(rep) = unary_op.cast_rep() { + format!("{}{}", token, rep) + } else { + format!("{}", token) + } +} + +// FIXME - adopt pretty-printing engine with precedence rules +fn show_expr(expr: &Expr) -> String { + match expr { + Expr::Const(typed_const) => format!("{}", typed_const), + Expr::BinOp(bin_op, expr, expr1) => format!("({} {} {})", show_expr(expr), show_bin_op(bin_op), show_expr(expr1)), + Expr::UnaryOp(unary_op, expr) => format!("{}({})", show_unary_op(unary_op), show_expr(expr)), + Expr::Cast(num_rep, expr) => format!("{} as {}", show_expr(expr), num_rep), + } +} + +// FIXME - adopt pretty-printing engine with precedence rules +fn show_typed_expr(t_expr: &TypedExpr) -> String { + match t_expr { + TypedExpr::ElabConst(t, typed_const) => { + format!("({}: {})", typed_const, t) + } + TypedExpr::ElabBinOp(t, typed_bin_op, typed_expr, typed_expr1) => { + format!("({} {} {} : {})", show_typed_expr(typed_expr), show_typed_bin_op(typed_bin_op), show_typed_expr(typed_expr1), t) + } + TypedExpr::ElabUnaryOp(t, typed_unary_op, typed_expr) => { + format!("({}({}) : {})", show_typed_expr(typed_expr), show_typed_unary_op(typed_unary_op), t) + } + TypedExpr::ElabCast(t, num_rep, typed_expr) => { + format!("({} as {} : {})", show_typed_expr(typed_expr), num_rep, t) + } + } +} + +pub fn print_conversion(expr: &Expr) { + let mut ie = InferenceEngine::new(); + match ie.infer_var_expr(expr) { + Ok(_) => { + let mut elab = Elaborator::new(ie); + let t_expr = elab.elaborate_expr(expr); + println!("Raw: {}", show_expr(expr)); + println!("Elaborated: {}", show_typed_expr(&t_expr)); + } + Err(e) => { + eprintln!("Inference failed ({}) on {:?}", e, expr); + } + } +} + + + + + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::TypedConst; + + #[test] + fn test_print_conversion() { + let expr = { + Expr::BinOp( + BinOp { op: BasicBinOp::Add, out_rep: None }, + Box::new( + Expr::BinOp( BinOp { op: BasicBinOp::Add, out_rep: Some(NumRep::U32) }, + Box::new(Expr::Const(TypedConst(BigInt::from(10), NumRep::U32))), + Box::new(Expr::Const(TypedConst(BigInt::from(-1), NumRep::I32))), + )), + Box::new( + Expr::Const(TypedConst(BigInt::from(5), NumRep::Auto)) + ) + ) + }; + print_conversion( + &expr + ) + } + +} From f3afd74fc1947ed2d84c4fe419f0a356e9518fba Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 4 Nov 2024 11:22:15 +1100 Subject: [PATCH 05/20] Add parser crate with REPL for analytic-engine --- Cargo.lock | 691 +++++++++++++++++- Cargo.toml | 2 +- experiments/analytic-engine/Cargo.toml | 1 - experiments/analytic-engine/src/core.rs | 46 +- experiments/analytic-engine/src/elaborator.rs | 263 ++++--- experiments/analytic-engine/src/lib.rs | 4 - experiments/analytic-engine/src/printer.rs | 82 ++- experiments/analytic-parser/Cargo.toml | 13 + experiments/analytic-parser/build.rs | 3 + experiments/analytic-parser/src/expr.lalrpop | 72 ++ experiments/analytic-parser/src/main.rs | 36 + 11 files changed, 1050 insertions(+), 163 deletions(-) create mode 100644 experiments/analytic-parser/Cargo.toml create mode 100644 experiments/analytic-parser/build.rs create mode 100644 experiments/analytic-parser/src/expr.lalrpop create mode 100644 experiments/analytic-parser/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index e9195291..d9726357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,12 +15,22 @@ dependencies = [ name = "analytic-engine" version = "0.1.0" dependencies = [ - "bitflags 2.6.0", "num-bigint", "num-traits", "proptest", ] +[[package]] +name = "analytic-parser" +version = "0.1.0" +dependencies = [ + "analytic-engine", + "lalrpop", + "lalrpop-util 0.21.0", + "linefeed", + "num-bigint", +] + [[package]] name = "anes" version = "0.1.6" @@ -73,19 +83,55 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -94,6 +140,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -106,6 +158,26 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -214,6 +286,21 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.5.1" @@ -226,7 +313,7 @@ dependencies = [ "clap", "criterion-plot", "is-terminal", - "itertools", + "itertools 0.10.5", "num-traits", "once_cell", "oorandom", @@ -247,7 +334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -281,6 +368,57 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" +dependencies = [ + "libc", + "redox_users 0.3.5", + "winapi", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dissimilar" version = "1.0.6" @@ -326,6 +464,15 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encoding" version = "0.2.33" @@ -390,6 +537,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.0" @@ -430,12 +583,39 @@ dependencies = [ "instant", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -444,7 +624,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -457,6 +637,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + [[package]] name = "heck" version = "0.4.1" @@ -469,6 +655,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -510,6 +715,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -525,6 +739,56 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lalrpop" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06093b57658c723a21da679530e061a8c25340fa5a6f98e313b542268c7e2a1f" +dependencies = [ + "ascii-canvas", + "bit-set 0.8.0", + "ena", + "itertools 0.13.0", + "lalrpop-util 0.22.0", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "108dc8f5dabad92c65a03523055577d847f5dcc00f3e7d3a68bc4d48e01d8fe1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feee752d43abd0f4807a921958ab4131f692a44d4d599733d4419c5d586176ce" +dependencies = [ + "regex-automata", + "rustversion", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -533,9 +797,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" @@ -543,12 +807,43 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linefeed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28715d08e35c6c074f9ae6b2e6a2420bac75d050c66ecd669d7d5b98e2caa036" +dependencies = [ + "dirs 1.0.5", + "mortal", + "winapi", +] + [[package]] name = "linux-raw-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.21" @@ -561,13 +856,61 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mortal" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c624fa1b7aab6bd2aff6e9b18565cc0363b6d45cbcd7465c9ed5e3740ebf097" +dependencies = [ + "bitflags 2.6.0", + "libc", + "nix", + "smallstr", + "terminfo", + "unicode-normalization", + "unicode-width", + "winapi", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -583,9 +926,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -603,6 +946,92 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.7", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "plotters" version = "0.3.6" @@ -637,6 +1066,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro2" version = "1.0.56" @@ -652,8 +1087,8 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ - "bit-set", - "bit-vec", + "bit-set 0.5.3", + "bit-vec 0.6.3", "bitflags 2.6.0", "lazy_static", "num-traits", @@ -708,7 +1143,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.8", ] [[package]] @@ -740,6 +1175,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.3.5" @@ -749,6 +1190,37 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +dependencies = [ + "getrandom 0.1.16", + "redox_syscall 0.1.57", + "rust-argon2", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.8", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.5" @@ -778,6 +1250,18 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + [[package]] name = "rustix" version = "0.37.7" @@ -792,6 +1276,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "rusty-fork" version = "0.3.0" @@ -819,6 +1309,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.159" @@ -850,6 +1346,50 @@ dependencies = [ "serde", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smallstr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.10.0" @@ -875,11 +1415,54 @@ checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys 0.45.0", ] +[[package]] +name = "term" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4175de05129f31b80458c6df371a15e7fc3fd367272e6bf938e5c351c7ea0" +dependencies = [ + "home", + "windows-sys 0.52.0", +] + +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -890,6 +1473,27 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unarray" version = "0.1.4" @@ -902,12 +1506,39 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -927,6 +1558,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -997,6 +1634,22 @@ dependencies = [ "wasm-bindgen", ] +[[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-util" version = "0.1.8" @@ -1006,6 +1659,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 576df92a..2c1ab2a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "generated/", "doodle-formats/", "experiments/analytic-engine/"] +members = [".", "generated/", "doodle-formats/", "experiments/analytic-engine/", "experiments/analytic-parser"] [package] name = "doodle" diff --git a/experiments/analytic-engine/Cargo.toml b/experiments/analytic-engine/Cargo.toml index 556d99b4..9c70e384 100644 --- a/experiments/analytic-engine/Cargo.toml +++ b/experiments/analytic-engine/Cargo.toml @@ -10,5 +10,4 @@ bench = false [dependencies] num-bigint = "0.4" num-traits = "0.2" -bitflags = "2.6" proptest = "1.5.0" diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index f4c53c8e..fec6fae4 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -1,5 +1,5 @@ use num_bigint::BigInt; -use num_traits::{One, Signed, Zero}; +use num_traits::{Signed, Zero}; use std::borrow::Cow; pub type Number = BigInt; @@ -83,9 +83,6 @@ impl NumRep { pub const AUTO: NumRep = NumRep::Auto; } - - - /// Representative min and max bounds for a numeric type #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] pub struct Bounds { @@ -105,7 +102,10 @@ impl Bounds { } pub fn singleton(n: Number) -> Self { - Self { min: n.clone(), max: n } + Self { + min: n.clone(), + max: n, + } } /// Returns `true` if every value in `sub_range` is also within `self`. @@ -149,7 +149,7 @@ macro_rules! bounds_of { impl NumRep { pub(crate) fn as_bounds(&self) -> Option { let (min, max) = match self { - NumRep::Auto => return None, + NumRep::Auto => return None, &NumRep::U8 => bounds_of!(u8), &NumRep::U16 => bounds_of!(u16), &NumRep::U32 => bounds_of!(u32), @@ -278,12 +278,16 @@ impl std::fmt::Display for Value { #[derive(Clone, Copy, Debug)] pub struct BinOp { - pub op: BasicBinOp, + op: BasicBinOp, // If None: op(T, T | auto) -> T, op(T0, T1) { T0 != T1 } -> ambiguous; otherwise, forces rep for `Some(rep)`` - pub out_rep: Option, + out_rep: Option, } impl BinOp { + pub const fn new(op: BasicBinOp, out_rep: Option) -> Self { + Self { op, out_rep } + } + pub fn output_type(&self, left: NumRep, right: NumRep) -> Option { if let Some(rep) = self.out_rep { Some(rep) @@ -315,7 +319,12 @@ pub struct UnaryOp { // If None, will pick the same type as the input (even if this produces a temporary unrepresentable) out_rep: Option, } + impl UnaryOp { + pub const fn new(op: BasicUnaryOp, out_rep: Option) -> Self { + Self { op, out_rep } + } + fn output_type(&self, in_rep: NumRep) -> NumRep { if let Some(rep) = self.out_rep { rep @@ -354,9 +363,7 @@ impl Expr { Expr::BinOp(bin_op, expr, expr1) => { bin_op.output_type(expr.get_rep()?, expr1.get_rep()?) } - Expr::UnaryOp(unary_op, expr) => { - Some(unary_op.output_type(expr.get_rep()?)) - } + Expr::UnaryOp(unary_op, expr) => Some(unary_op.output_type(expr.get_rep()?)), } } } @@ -412,10 +419,9 @@ impl Expr { } else { return Err(EvalError::RemainderNonPositive); } - } - // (_, Value::Opt(..), _) | (_, _, Value::Opt(..)) => { - // return Err(EvalError::ArithOrCastOption) - // } + } // (_, Value::Opt(..), _) | (_, _, Value::Opt(..)) => { + // return Err(EvalError::ArithOrCastOption) + // } }; let rep_out = match out_rep { Some(rep) => *rep, @@ -425,7 +431,7 @@ impl Expr { } else if rep0.is_auto() { rep1 } else { - return Err(EvalError::Ambiguous(rep0, rep1)) + return Err(EvalError::Ambiguous(rep0, rep1)); } } }; @@ -448,7 +454,6 @@ impl Expr { }; Ok(Value::Const(TypedConst(n.abs(), rep_out))) } - // (_, Value::Opt(_)) => return Err(EvalError::ArithOrCastOption), } } Expr::Cast(num_rep, expr) => { @@ -457,14 +462,8 @@ impl Expr { Value::Const(TypedConst(num, _rep)) => { Ok(Value::Const(TypedConst(num, *num_rep))) } - // Value::Opt(_) => return Err(EvalError::ArithOrCastOption), } } - // Expr::TryUnwrap(expr) => match expr.eval()? { - // Value::Const(_) => return Err(EvalError::TryUnwrapConst), - // Value::Opt(None) => return Err(EvalError::TryUnwrapNone), - // Value::Opt(Some(x)) => Ok(*x), - // }, } } } @@ -472,6 +471,7 @@ impl Expr { #[cfg(test)] mod tests { use crate::core::*; + use num_traits::One; use proptest::prelude::*; fn abstract_strategy() -> BoxedStrategy { diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs index 6eccda8f..99324bb8 100644 --- a/experiments/analytic-engine/src/elaborator.rs +++ b/experiments/analytic-engine/src/elaborator.rs @@ -1,4 +1,4 @@ -use crate::core::{Expr, NumRep, Value, TypedConst, UnaryOp, BinOp, BasicBinOp, BasicUnaryOp}; +use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, Expr, NumRep, TypedConst, UnaryOp, Value}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub(crate) enum PrimInt { @@ -12,7 +12,16 @@ pub(crate) enum PrimInt { I64, } -pub const PRIM_INTS: [PrimInt; 8] = [PrimInt::U8, PrimInt::U16, PrimInt::U32, PrimInt::U64, PrimInt::I8, PrimInt::I16, PrimInt::I32, PrimInt::I64]; +pub const PRIM_INTS: [PrimInt; 8] = [ + PrimInt::U8, + PrimInt::U16, + PrimInt::U32, + PrimInt::U64, + PrimInt::I8, + PrimInt::I16, + PrimInt::I32, + PrimInt::I64, +]; #[derive(Debug)] pub struct TryFromAutoError; @@ -58,7 +67,6 @@ impl From for NumRep { } } - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) enum IntType { Prim(PrimInt), @@ -72,11 +80,15 @@ impl std::fmt::Display for IntType { } } - #[derive(Clone, Debug)] pub(crate) enum TypedExpr { ElabConst(TypeRep, TypedConst), - ElabBinOp(TypeRep, TypedBinOp, Box>, Box>), + ElabBinOp( + TypeRep, + TypedBinOp, + Box>, + Box>, + ), ElabUnaryOp(TypeRep, TypedUnaryOp, Box>), ElabCast(TypeRep, NumRep, Box>), } @@ -90,7 +102,6 @@ impl TypedExpr { TypedExpr::ElabCast(t, _, _) => t, } } - } type Sig1 = (T, T); @@ -238,7 +249,6 @@ pub(crate) mod inference { Encompasses(crate::core::Bounds), } - impl std::fmt::Display for Constraint { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -248,7 +258,6 @@ pub(crate) mod inference { } } - impl From for Constraint { fn from(value: UType) -> Self { Constraint::Equiv(value) @@ -265,10 +274,16 @@ pub(crate) mod inference { Constraint::Equiv(utype) => match utype { UType::Var(_) => None, UType::Int(int_type) => Some(int_type == &candidate), - } + }, Constraint::Encompasses(bounds) => { let IntType::Prim(candidate) = candidate; - Some(bounds.is_encompassed_by(&>::into(candidate).as_bounds().unwrap())) + Some( + bounds.is_encompassed_by( + &>::into(candidate) + .as_bounds() + .unwrap(), + ), + ) } } } @@ -309,7 +324,6 @@ pub(crate) mod inference { } } - #[derive(Debug)] pub struct InferenceEngine { constraints: Vec, @@ -390,7 +404,11 @@ pub(crate) mod inference { } } - fn unify_var_constraint(&mut self, uvar: UVar, constraint: Constraint) -> InferenceResult { + fn unify_var_constraint( + &mut self, + uvar: UVar, + constraint: Constraint, + ) -> InferenceResult { let can_ix = self.get_canonical_uvar(uvar).0; match &self.constraints[can_ix] { @@ -416,7 +434,11 @@ pub(crate) mod inference { self.aliases[lo].add_forward_ref(hi); } - unsafe fn transfer_constraints(&mut self, a1: usize, a2: usize) -> InferenceResult<&Constraints> { + unsafe fn transfer_constraints( + &mut self, + a1: usize, + a2: usize, + ) -> InferenceResult<&Constraints> { if a1 == a2 { return Ok(&self.constraints[a1]); } @@ -427,7 +449,8 @@ pub(crate) mod inference { (_, Constraints::Indefinite) => Ok(self.replace_constraints_from_index(a2, a1)), (Constraints::Invariant(c1), Constraints::Invariant(c2)) => { let c0 = self.unify_constraint_pair(c1.clone(), c2.clone())?; - let _ = self.replace_constraints_with_value(a1, Constraints::Invariant(c0.clone())); + let _ = + self.replace_constraints_with_value(a1, Constraints::Invariant(c0.clone())); let _ = self.replace_constraints_with_value(a2, Constraints::Invariant(c0)); Ok(&self.constraints[a1]) } @@ -447,7 +470,6 @@ pub(crate) mod inference { &self.constraints[ix] } - fn unify_var_pair(&mut self, v1: UVar, v2: UVar) -> InferenceResult<&Constraints> { if v1 == v2 { return Ok(&self.constraints[v1.0]); @@ -471,7 +493,7 @@ pub(crate) mod inference { (Alias::Ground, &Alias::BackRef(can_ix)) if v1.0 > can_ix => unsafe { self.repoint(can_ix, v1.0); self.transfer_constraints(can_ix, v1.0) - } + }, (Alias::Ground, &Alias::BackRef(can_ix)) if v1.0 < can_ix => { debug_assert!( self.aliases[can_ix].is_canonical_nonempty(), @@ -489,7 +511,7 @@ pub(crate) mod inference { (&Alias::BackRef(can_ix), Alias::Ground) if v2.0 > can_ix => unsafe { self.repoint(can_ix, v2.0); self.transfer_constraints(can_ix, v2.0) - } + }, (&Alias::BackRef(can_ix), Alias::Ground) if v2.0 < can_ix => { debug_assert!( self.aliases[can_ix].is_canonical_nonempty(), @@ -534,10 +556,10 @@ pub(crate) mod inference { } (&Alias::BackRef(ix1), &Alias::BackRef(ix2)) if ix1 < ix2 => unsafe { self.recanonicalize(ix1, ix2) - } + }, (&Alias::BackRef(ix1), &Alias::BackRef(ix2)) if ix2 < ix1 => unsafe { self.recanonicalize(ix2, ix1) - } + }, (&Alias::BackRef(ix), &Alias::BackRef(_ix)) => { // the two are equal so nothing needs to be changed; we will check both are forward-aliased, however let common = &self.aliases[ix]; @@ -596,7 +618,6 @@ pub(crate) mod inference { let ix1 = v1.0; let ix2 = *tgt; - // check not the actual indices, but the canonical indices for tie-breaking if ix1 < ix2 { unsafe { self.recanonicalize(ix1, ix2) } @@ -653,29 +674,42 @@ pub(crate) mod inference { unreachable!("equiv should erase encompasses") } } - } (UType::Int(t0), UType::Int(t1)) => { if t0 != t1 { - return Err(InferenceError::BadUnification(Constraint::Equiv(left), Constraint::Equiv(right))); + return Err(InferenceError::BadUnification( + Constraint::Equiv(left), + Constraint::Equiv(right), + )); } Ok(left) } } } - fn unify_utype_bounds(&mut self, utype: UType, bounds: &Bounds) -> InferenceResult { + fn unify_utype_bounds( + &mut self, + utype: UType, + bounds: &Bounds, + ) -> InferenceResult { match utype { UType::Var(uvar) => { self.unify_var_constraint(uvar, Constraint::Encompasses(bounds.clone())) } UType::Int(int_type) => { let IntType::Prim(candidate) = int_type; - let soluble = bounds.is_encompassed_by(&>::into(candidate).as_bounds().unwrap()); + let soluble = bounds.is_encompassed_by( + &>::into(candidate) + .as_bounds() + .unwrap(), + ); if soluble { - Ok(Constraint::Equiv(utype)) + Ok(Constraint::Equiv(utype)) } else { - Err(InferenceError::BadUnification(Constraint::Equiv(utype), Constraint::Encompasses(bounds.clone()))) + Err(InferenceError::BadUnification( + Constraint::Equiv(utype), + Constraint::Encompasses(bounds.clone()), + )) } } } @@ -686,7 +720,6 @@ pub(crate) mod inference { Ok(()) } - fn unify_var_rep(&mut self, uvar: UVar, rep: NumRep) -> InferenceResult<()> { if rep.is_auto() { return Ok(()); @@ -695,7 +728,11 @@ pub(crate) mod inference { self.unify_var_utype(uvar, t) } - fn unify_constraint_pair(&mut self, c1: Constraint, c2: Constraint) -> InferenceResult { + fn unify_constraint_pair( + &mut self, + c1: Constraint, + c2: Constraint, + ) -> InferenceResult { match (c1, c2) { (Constraint::Equiv(t1), Constraint::Equiv(t2)) => { if t1 == t2 { @@ -725,17 +762,46 @@ pub(crate) mod inference { let var = match rep { NumRep::AUTO => { let this_var = self.get_new_uvar(); - self.unify_var_constraint(this_var, Constraint::Encompasses(Bounds::singleton(typed_const.as_raw_value().clone())))?; + self.unify_var_constraint( + this_var, + Constraint::Encompasses(Bounds::singleton( + typed_const.as_raw_value().clone(), + )), + )?; this_var } - NumRep::U8 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U8)))?.0, - NumRep::U16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U16)))?.0, - NumRep::U32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U32)))?.0, - NumRep::U64 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U64)))?.0, - NumRep::I8 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I8)))?.0, - NumRep::I16 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I16)))?.0, - NumRep::I32 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I32)))?.0, - NumRep::I64 => self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I64)))?.0, + NumRep::U8 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U8)))? + .0 + } + NumRep::U16 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U16)))? + .0 + } + NumRep::U32 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U32)))? + .0 + } + NumRep::U64 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::U64)))? + .0 + } + NumRep::I8 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I8)))? + .0 + } + NumRep::I16 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I16)))? + .0 + } + NumRep::I32 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I32)))? + .0 + } + NumRep::I64 => { + self.init_var_simple(UType::Int(IntType::Prim(PrimInt::I64)))? + .0 + } }; (var, rep) } @@ -761,18 +827,22 @@ pub(crate) mod inference { Ok(v) => match v.as_const() { Some(c) => { // NOTE - if there is a const-evaluable result for the computation, use it to refine our constraints on which types satisfy the aliased Auto - let bounds = Bounds::singleton(c.as_raw_value().clone()); - self.unify_var_constraint(this_var, Constraint::Encompasses(bounds))?; + let bounds = + Bounds::singleton(c.as_raw_value().clone()); + self.unify_var_constraint( + this_var, + Constraint::Encompasses(bounds), + )?; } None => { // FIXME - this isn't a hard error necessarily, but our model isn't complex enough for non-TypedConst values to emerge unimplemented!("Value::AsConst returned None (unexpectedly) when called from InferenceEngine::infer_var_expr"); } - } + }, Err(e) => { // NOTE - If the computation will fail regardless, there is no need to infer the type-information of the AST // REVIEW - make sure that we are confident in EvalErrors being sound reasons to fail type-inference, both now and going forward - return Err(InferenceError::Eval(e)) + return Err(InferenceError::Eval(e)); } } } @@ -815,7 +885,6 @@ pub(crate) mod inference { } } } - }; (this_var, this_rep) } @@ -839,18 +908,22 @@ pub(crate) mod inference { Ok(v) => match v.as_const() { Some(c) => { // NOTE - if there is a const-evaluable result for the computation, use it to refine our constraints on which types satisfy the aliased Auto - let bounds = Bounds::singleton(c.as_raw_value().clone()); - self.unify_var_constraint(this_var, Constraint::Encompasses(bounds))?; + let bounds = + Bounds::singleton(c.as_raw_value().clone()); + self.unify_var_constraint( + this_var, + Constraint::Encompasses(bounds), + )?; } None => { // FIXME - this isn't a hard error necessarily, but our model isn't complex enough for non-TypedConst values to emerge unimplemented!("Value::AsConst returned None (unexpectedly) when called from InferenceEngine::infer_var_expr"); } - } + }, Err(e) => { // NOTE - If the computation will fail regardless, there is no need to infer the type-information of the AST // REVIEW - make sure that we are confident in EvalErrors being sound reasons to fail type-inference, both now and going forward - return Err(InferenceError::Eval(e)) + return Err(InferenceError::Eval(e)); } } } @@ -892,7 +965,9 @@ pub(crate) mod inference { match &self.constraints[v0.0] { Constraints::Indefinite => VType::Abstract(v0.into()), Constraints::Invariant(Constraint::Equiv(ut)) => self.to_whnf_vtype(*ut), - Constraints::Invariant(Constraint::Encompasses(bounds)) => VType::Within(bounds.clone()), + Constraints::Invariant(Constraint::Encompasses(bounds)) => { + VType::Within(bounds.clone()) + } } } UType::Int(int_type) => VType::Int(int_type), @@ -904,8 +979,8 @@ pub(crate) mod inference { Constraints::Indefinite => Ok(None), Constraints::Invariant(cx) => Ok(match cx { Constraint::Equiv(ut) => Some(self.to_whnf_vtype(*ut)), - Constraint::Encompasses(bounds) => Some(VType::Within(bounds.clone())) - }) + Constraint::Encompasses(bounds) => Some(VType::Within(bounds.clone())), + }), } } } @@ -918,18 +993,18 @@ pub(crate) mod inference { match self.substitute_uvar_vtype(v) { Ok(Some(t0)) => match t0 { VType::Int(int_type) => Some(int_type), - VType::Within(bounds) => match Constraint::get_unique_solution(&Constraint::Encompasses(bounds.clone())) { + VType::Within(bounds) => match Constraint::get_unique_solution( + &Constraint::Encompasses(bounds.clone()), + ) { Ok(int_type) => Some(int_type), Err(_) => None, - } + }, VType::Abstract(utype) => self.reify(utype), - } + }, Err(_) => None, - Ok(None) => { - match &self.constraints[v.0] { - _ => None, - } - } + Ok(None) => match &self.constraints[v.0] { + _ => None, + }, } } UType::Int(i) => Some(i), @@ -940,50 +1015,64 @@ pub(crate) mod inference { use inference::{InferenceEngine, UVar}; +/// Alias for whatever value-type we use to associate a failed reification with some indication of what went wrong, or where +type Hint = usize; + +#[derive(Debug)] +pub enum ElaborationError { + BadReification(Hint), +} + +impl std::fmt::Display for ElaborationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElaborationError::BadReification(hint) => { + write!(f, "bad reification on UVar ?{}", hint) + } + } + } +} + +impl std::error::Error for ElaborationError {} + +pub(crate) type ElaborationResult = Result; + pub struct Elaborator { next_index: usize, ie: InferenceEngine, } impl Elaborator { + pub(crate) fn new(ie: InferenceEngine) -> Self { + Self { next_index: 0, ie } + } + fn get_and_increment_index(&mut self) -> usize { let ret = self.next_index; self.next_index += 1; ret } - fn increment_index(&mut self) { - self.next_index += 1; - } - - fn get_index(&self) -> usize { - self.next_index - } - - pub(crate) fn new(ie: InferenceEngine) -> Self { - Self { next_index: 0, ie } - } - - fn get_type_from_index(&self, index: usize) -> IntType { + fn get_type_from_index(&self, index: usize) -> ElaborationResult { let uvar = UVar::new(index); let Some(t) = self.ie.reify(uvar.into()) else { - unreachable!("unable to reify {uvar}") + return Err(ElaborationError::BadReification(index)); }; - t + Ok(t) } - pub(crate) fn elaborate_expr(&mut self, expr: &Expr) -> TypedExpr { + pub(crate) fn elaborate_expr(&mut self, expr: &Expr) -> ElaborationResult> { let index = self.get_and_increment_index(); match expr { Expr::Const(typed_const) => { - let t = self.get_type_from_index(index); - TypedExpr::ElabConst(t, typed_const.clone()) + let t = self.get_type_from_index(index)?; + Ok(TypedExpr::ElabConst(t, typed_const.clone())) } Expr::BinOp(bin_op, x, y) => { - let t_x = self.elaborate_expr(x); - let t_y = self.elaborate_expr(y); - let t = self.get_type_from_index(index); - TypedExpr::ElabBinOp( + let t_x = self.elaborate_expr(x)?; + let t_y = self.elaborate_expr(y)?; + let t = self.get_type_from_index(index)?; + Ok(TypedExpr::ElabBinOp( t, TypedBinOp { sig: ((*t_x.get_type(), *t_y.get_type()), t), @@ -991,24 +1080,24 @@ impl Elaborator { }, Box::new(t_x), Box::new(t_y), - ) + )) } Expr::UnaryOp(unary_op, inner) => { - let t_inner = self.elaborate_expr(inner); - let t = self.get_type_from_index(index); - TypedExpr::ElabUnaryOp( + let t_inner = self.elaborate_expr(inner)?; + let t = self.get_type_from_index(index)?; + Ok(TypedExpr::ElabUnaryOp( t, TypedUnaryOp { sig: (*t_inner.get_type(), t), inner: *unary_op, }, Box::new(t_inner), - ) + )) } Expr::Cast(rep, inner) => { - let t_inner = self.elaborate_expr(inner); - let t = self.get_type_from_index(index); - TypedExpr::ElabCast(t, *rep, Box::new(t_inner)) + let t_inner = self.elaborate_expr(inner)?; + let t = self.get_type_from_index(index)?; + Ok(TypedExpr::ElabCast(t, *rep, Box::new(t_inner))) } } } diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs index 14972267..7c58aca7 100644 --- a/experiments/analytic-engine/src/lib.rs +++ b/experiments/analytic-engine/src/lib.rs @@ -1,7 +1,3 @@ -extern crate proptest; -extern crate num_bigint; -extern crate num_traits; - pub mod core; pub mod elaborator; pub mod printer; diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index 52c15332..fb75c925 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -1,7 +1,6 @@ -use crate::elaborator::{TypedExpr, TypedBinOp, TypedUnaryOp, IntType, PrimInt, Elaborator}; -use num_bigint::BigInt; -use crate::core::{BasicBinOp, BinOp, Expr, NumRep, UnaryOp}; +use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, Expr, UnaryOp}; use crate::elaborator::inference::InferenceEngine; +use crate::elaborator::{Elaborator, IntType, TypedBinOp, TypedExpr, TypedUnaryOp}; fn show_bin_op(bin_op: &BinOp) -> String { let token = match bin_op.get_op() { @@ -26,22 +25,24 @@ fn show_typed_bin_op(bin_op: &TypedBinOp) -> String { BasicBinOp::Div => "/", BasicBinOp::Rem => "%", }; - format!("({} : ({},{}) -> {})", token, bin_op.sig.0.0, bin_op.sig.0.1, bin_op.sig.1) + format!( + "({} : ({},{}) -> {})", + token, bin_op.sig.0 .0, bin_op.sig.0 .1, bin_op.sig.1 + ) } fn show_typed_unary_op(unary_op: &TypedUnaryOp) -> String { let token = match unary_op.inner.get_op() { - crate::core::BasicUnaryOp::Negate => "~", - crate::core::BasicUnaryOp::AbsVal => "abs", + BasicUnaryOp::Negate => "~", + BasicUnaryOp::AbsVal => "abs", }; format!("({} : {} -> {})", token, unary_op.sig.1, unary_op.sig.1) } - fn show_unary_op(unary_op: &UnaryOp) -> String { let token = match unary_op.get_op() { - crate::core::BasicUnaryOp::Negate => "~", - crate::core::BasicUnaryOp::AbsVal => "abs", + BasicUnaryOp::Negate => "~", + BasicUnaryOp::AbsVal => "abs", }; if let Some(rep) = unary_op.cast_rep() { format!("{}{}", token, rep) @@ -51,11 +52,18 @@ fn show_unary_op(unary_op: &UnaryOp) -> String { } // FIXME - adopt pretty-printing engine with precedence rules -fn show_expr(expr: &Expr) -> String { +pub fn show_expr(expr: &Expr) -> String { match expr { Expr::Const(typed_const) => format!("{}", typed_const), - Expr::BinOp(bin_op, expr, expr1) => format!("({} {} {})", show_expr(expr), show_bin_op(bin_op), show_expr(expr1)), - Expr::UnaryOp(unary_op, expr) => format!("{}({})", show_unary_op(unary_op), show_expr(expr)), + Expr::BinOp(bin_op, expr, expr1) => format!( + "({} {} {})", + show_expr(expr), + show_bin_op(bin_op), + show_expr(expr1) + ), + Expr::UnaryOp(unary_op, expr) => { + format!("{}({})", show_unary_op(unary_op), show_expr(expr)) + } Expr::Cast(num_rep, expr) => format!("{} as {}", show_expr(expr), num_rep), } } @@ -67,10 +75,21 @@ fn show_typed_expr(t_expr: &TypedExpr) -> String { format!("({}: {})", typed_const, t) } TypedExpr::ElabBinOp(t, typed_bin_op, typed_expr, typed_expr1) => { - format!("({} {} {} : {})", show_typed_expr(typed_expr), show_typed_bin_op(typed_bin_op), show_typed_expr(typed_expr1), t) + format!( + "({} {} {} : {})", + show_typed_expr(typed_expr), + show_typed_bin_op(typed_bin_op), + show_typed_expr(typed_expr1), + t + ) } TypedExpr::ElabUnaryOp(t, typed_unary_op, typed_expr) => { - format!("({}({}) : {})", show_typed_expr(typed_expr), show_typed_unary_op(typed_unary_op), t) + format!( + "({}({}) : {})", + show_typed_expr(typed_expr), + show_typed_unary_op(typed_unary_op), + t + ) } TypedExpr::ElabCast(t, num_rep, typed_expr) => { format!("({} as {} : {})", show_typed_expr(typed_expr), num_rep, t) @@ -83,9 +102,19 @@ pub fn print_conversion(expr: &Expr) { match ie.infer_var_expr(expr) { Ok(_) => { let mut elab = Elaborator::new(ie); - let t_expr = elab.elaborate_expr(expr); - println!("Raw: {}", show_expr(expr)); - println!("Elaborated: {}", show_typed_expr(&t_expr)); + match elab.elaborate_expr(expr) { + Ok(t_expr) => { + println!("Raw: {}", show_expr(expr)); + println!("Elaborated: {}", show_typed_expr(&t_expr)); + } + Err(elab_err) => { + eprintln!( + "Error encountered during elaboration of `{}`: {}", + show_expr(expr), + elab_err + ); + } + } } Err(e) => { eprintln!("Inference failed ({}) on {:?}", e, expr); @@ -93,10 +122,6 @@ pub fn print_conversion(expr: &Expr) { } } - - - - #[cfg(test)] mod tests { use super::*; @@ -106,20 +131,15 @@ mod tests { fn test_print_conversion() { let expr = { Expr::BinOp( - BinOp { op: BasicBinOp::Add, out_rep: None }, - Box::new( - Expr::BinOp( BinOp { op: BasicBinOp::Add, out_rep: Some(NumRep::U32) }, + BinOp::new(BasicBinOp::Add, None), + Box::new(Expr::BinOp( + BinOp::new(BasicBinOp::Add, Some(NumRep::U32)), Box::new(Expr::Const(TypedConst(BigInt::from(10), NumRep::U32))), Box::new(Expr::Const(TypedConst(BigInt::from(-1), NumRep::I32))), )), - Box::new( - Expr::Const(TypedConst(BigInt::from(5), NumRep::Auto)) - ) + Box::new(Expr::Const(TypedConst(BigInt::from(5), NumRep::Auto))), ) }; - print_conversion( - &expr - ) + print_conversion(&expr) } - } diff --git a/experiments/analytic-parser/Cargo.toml b/experiments/analytic-parser/Cargo.toml new file mode 100644 index 00000000..7012836a --- /dev/null +++ b/experiments/analytic-parser/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "analytic-parser" +version = "0.1.0" +edition = "2021" + +[build-dependencies] +lalrpop = "0.22.0" + +[dependencies] +lalrpop-util = { version = "0.21.0", features = ["lexer", "unicode"] } +analytic-engine = { path = "../analytic-engine/" } +num-bigint = "0.4.6" +linefeed = "0.6.0" diff --git a/experiments/analytic-parser/build.rs b/experiments/analytic-parser/build.rs new file mode 100644 index 00000000..ca5c2836 --- /dev/null +++ b/experiments/analytic-parser/build.rs @@ -0,0 +1,3 @@ +fn main() { + lalrpop::process_root().unwrap(); +} diff --git a/experiments/analytic-parser/src/expr.lalrpop b/experiments/analytic-parser/src/expr.lalrpop new file mode 100644 index 00000000..95678922 --- /dev/null +++ b/experiments/analytic-parser/src/expr.lalrpop @@ -0,0 +1,72 @@ +use std::str::FromStr; +use num_bigint::BigInt; +use analytic_engine::core::{TypedConst, BasicBinOp, BinOp, BasicUnaryOp, UnaryOp, Expr, NumRep}; + +grammar; + +pub Tree: Expr = { + #[precedence(level="0")] + Term, + #[precedence(level="1")] + => Expr::UnaryOp(op, Box::new(t)), + #[precedence(level="2")] + "as" => Expr::Cast(r, Box::new(t)), + #[precedence(level="3")] #[assoc(side="left")] + => Expr::BinOp(op, Box::new(l), Box::new(r)), + #[precedence(level="4")] #[assoc(side="left")] + => Expr::BinOp(op, Box::new(l), Box::new(r)), +} + +Term: Expr = { + Const => Expr::Const(<>), + "(" ")", +} + +OpCode0: BasicUnaryOp = { + "~" => BasicUnaryOp::Negate, + "abs" => BasicUnaryOp::AbsVal, +} + +OpCode1: BasicBinOp = { + "*" => BasicBinOp::Mul, + "/" => BasicBinOp::Div, + "%" => BasicBinOp::Rem, +} + +OpCode2: BasicBinOp = { + "+" => BasicBinOp::Add, + "-" => BasicBinOp::Sub, +} + +Op0: UnaryOp = { + => UnaryOp::new(op, Some(r)), + => UnaryOp::new(op, None), +} + +Op1: BinOp = { + => BinOp::new(op, Some(r)), + => BinOp::new(op, None), +} + +Op2: BinOp = { + => BinOp::new(op, Some(r)), + => BinOp::new(op, None), +} + +Const: TypedConst = { + => TypedConst(n, NumRep::AUTO), + => TypedConst(n, r), +} + +Rep: NumRep = { + "u8" => NumRep::U8, + "u16" => NumRep::U16, + "u32" => NumRep::U32, + "u64" => NumRep::U64, + "i8" => NumRep::I8, + "i16" => NumRep::I16, + "i32" => NumRep::I32, + "i64" => NumRep::I64, +} + +Num: BigInt = => BigInt::from_str(s).unwrap(); diff --git a/experiments/analytic-parser/src/main.rs b/experiments/analytic-parser/src/main.rs new file mode 100644 index 00000000..71448d9d --- /dev/null +++ b/experiments/analytic-parser/src/main.rs @@ -0,0 +1,36 @@ +use analytic_engine::printer::print_conversion; +use lalrpop_util::lalrpop_mod; +use linefeed::{Interface, ReadResult}; + +lalrpop_mod!( + #[allow(clippy::ptr_arg)] + #[rustfmt::skip] + pub expr +); + +#[cfg(test)] +mod tests { + use super::*; + use analytic_engine::printer::show_expr; + + #[test] + fn parse() { + let expr = expr::TreeParser::new().parse("1 + 2").unwrap(); + assert_eq!(&show_expr(&expr), "(1? + 2?)"); + } +} + +pub fn main() -> std::io::Result<()> { + let mut reader = Interface::new("analytic")?; + let parser = expr::TreeParser::new(); + reader.set_prompt("α> ")?; + while let ReadResult::Input(input) = reader.read_line()? { + let interp = parser.parse(&input); + match interp { + Ok(expr) => print_conversion(&expr), + Err(err) => eprintln!("[ERROR]: {}", err), + } + } + println!("Analysis Complete."); + Ok(()) +} From 611e8d0f87e82e13ca5a4bd10812b072048a6e06 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 4 Nov 2024 11:30:43 +1100 Subject: [PATCH 06/20] Fix bugs in show_typed_unary_op --- experiments/analytic-engine/src/core.rs | 34 +++++------ experiments/analytic-engine/src/elaborator.rs | 58 +++++++++---------- experiments/analytic-engine/src/printer.rs | 4 +- experiments/analytic-parser/src/main.rs | 2 +- 4 files changed, 49 insertions(+), 49 deletions(-) diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index fec6fae4..60040b8e 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -325,13 +325,13 @@ impl UnaryOp { Self { op, out_rep } } - fn output_type(&self, in_rep: NumRep) -> NumRep { - if let Some(rep) = self.out_rep { - rep - } else { - in_rep - } - } + // fn output_type(&self, in_rep: NumRep) -> NumRep { + // if let Some(rep) = self.out_rep { + // rep + // } else { + // in_rep + // } + // } pub fn cast_rep(&self) -> Option { self.out_rep @@ -356,16 +356,16 @@ pub enum Expr { } impl Expr { - pub(crate) fn get_rep(&self) -> Option { - match self { - Expr::Const(tc) => Some(tc.get_rep()), - Expr::Cast(rep, _) => Some(*rep), - Expr::BinOp(bin_op, expr, expr1) => { - bin_op.output_type(expr.get_rep()?, expr1.get_rep()?) - } - Expr::UnaryOp(unary_op, expr) => Some(unary_op.output_type(expr.get_rep()?)), - } - } + // pub(crate) fn get_rep(&self) -> Option { + // match self { + // Expr::Const(tc) => Some(tc.get_rep()), + // Expr::Cast(rep, _) => Some(*rep), + // Expr::BinOp(bin_op, expr, expr1) => { + // bin_op.output_type(expr.get_rep()?, expr1.get_rep()?) + // } + // Expr::UnaryOp(unary_op, expr) => Some(unary_op.output_type(expr.get_rep()?)), + // } + // } } #[derive(Debug)] diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs index 99324bb8..505a9d64 100644 --- a/experiments/analytic-engine/src/elaborator.rs +++ b/experiments/analytic-engine/src/elaborator.rs @@ -1,4 +1,4 @@ -use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, Expr, NumRep, TypedConst, UnaryOp, Value}; +use crate::core::{BinOp, Expr, NumRep, TypedConst, UnaryOp}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] pub(crate) enum PrimInt { @@ -12,7 +12,7 @@ pub(crate) enum PrimInt { I64, } -pub const PRIM_INTS: [PrimInt; 8] = [ +pub(crate) const PRIM_INTS: [PrimInt; 8] = [ PrimInt::U8, PrimInt::U16, PrimInt::U32, @@ -122,7 +122,7 @@ pub(crate) struct TypedUnaryOp { pub(crate) mod inference { use std::collections::HashSet; - use crate::core::{Bounds, Expr, NumRep, TypedConst}; + use crate::core::{Bounds, Expr, NumRep}; use super::{IntType, PrimInt}; @@ -141,9 +141,9 @@ pub(crate) mod inference { Self(ix) } - pub fn to_usize(self) -> usize { - self.0 - } + // pub fn to_usize(self) -> usize { + // self.0 + // } } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] @@ -191,19 +191,19 @@ pub(crate) mod inference { } } - pub fn as_backref(&self) -> Option { - match self { - Alias::Ground | Alias::Canonical(_) => None, - Alias::BackRef(ix) => Some(*ix), - } - } + // pub fn as_backref(&self) -> Option { + // match self { + // Alias::Ground | Alias::Canonical(_) => None, + // Alias::BackRef(ix) => Some(*ix), + // } + // } pub fn add_forward_ref(&mut self, tgt: usize) { match self { Alias::Ground => { let _ = std::mem::replace(self, Alias::Canonical(HashSet::from([tgt]))); } - Alias::BackRef(ix) => panic!("cannot add forward-ref to Alias::BackRef"), + Alias::BackRef(_ix) => panic!("cannot add forward-ref to Alias::BackRef"), Alias::Canonical(fwds) => { fwds.insert(tgt); } @@ -288,20 +288,20 @@ pub(crate) mod inference { } } - pub(crate) fn has_unique_assignment(&self) -> bool { - // REVIEW - there are smarter ways of calculating this - let mut solutions = 0; - for prim_int in super::PRIM_INTS.iter() { - match self.is_satisfied_by(IntType::Prim(*prim_int)) { - Some(true) => { - solutions += 1; - } - Some(false) => (), - None => return false, - } - } - solutions == 1 - } + // pub(crate) fn has_unique_assignment(&self) -> bool { + // // REVIEW - there are smarter ways of calculating this + // let mut solutions = 0; + // for prim_int in super::PRIM_INTS.iter() { + // match self.is_satisfied_by(IntType::Prim(*prim_int)) { + // Some(true) => { + // solutions += 1; + // } + // Some(false) => (), + // None => return false, + // } + // } + // solutions == 1 + // } // NOTE - should only be called on Encompasses pub(crate) fn get_unique_solution(&self) -> InferenceResult { @@ -332,7 +332,7 @@ pub(crate) mod inference { #[derive(Debug)] pub enum InferenceError { - Unrepresentable(TypedConst, IntType), + // Unrepresentable(TypedConst, IntType), BadUnification(Constraint, Constraint), AbstractCast, Ambiguous, @@ -344,7 +344,7 @@ pub(crate) mod inference { impl std::fmt::Display for InferenceError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - InferenceError::Unrepresentable(c, int_type) => write!(f, "inference requires that `{}` be assigned type `{}`, which cannot represent it", c, int_type), + // InferenceError::Unrepresentable(c, int_type) => write!(f, "inference requires that `{}` be assigned type `{}`, which cannot represent it", c, int_type), InferenceError::BadUnification(cx1, cx2) => write!(f, "constraints `{}` and `{}` cannot be unified", cx1, cx2), InferenceError::AbstractCast => write!(f, "casts and operations cannot explicitly produce abstract NumReps"), InferenceError::Ambiguous => write!(f, "mixed-type binary operation must have out_rep on operation to avoid ambiguity"), diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index fb75c925..02b4654e 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -36,7 +36,7 @@ fn show_typed_unary_op(unary_op: &TypedUnaryOp) -> String { BasicUnaryOp::Negate => "~", BasicUnaryOp::AbsVal => "abs", }; - format!("({} : {} -> {})", token, unary_op.sig.1, unary_op.sig.1) + format!("({} : {} -> {})", token, unary_op.sig.0, unary_op.sig.1) } fn show_unary_op(unary_op: &UnaryOp) -> String { @@ -86,8 +86,8 @@ fn show_typed_expr(t_expr: &TypedExpr) -> String { TypedExpr::ElabUnaryOp(t, typed_unary_op, typed_expr) => { format!( "({}({}) : {})", - show_typed_expr(typed_expr), show_typed_unary_op(typed_unary_op), + show_typed_expr(typed_expr), t ) } diff --git a/experiments/analytic-parser/src/main.rs b/experiments/analytic-parser/src/main.rs index 71448d9d..bfd83bf9 100644 --- a/experiments/analytic-parser/src/main.rs +++ b/experiments/analytic-parser/src/main.rs @@ -21,7 +21,7 @@ mod tests { } pub fn main() -> std::io::Result<()> { - let mut reader = Interface::new("analytic")?; + let reader = Interface::new("analytic")?; let parser = expr::TreeParser::new(); reader.set_prompt("α> ")?; while let ReadResult::Input(input) = reader.read_line()? { From 2d0a22a5e10942922cc3fa7e544c365e26767ebe Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 4 Nov 2024 13:40:41 +1100 Subject: [PATCH 07/20] Add precedence and fragment-based printing --- experiments/analytic-engine/src/core.rs | 38 +- experiments/analytic-engine/src/elaborator.rs | 27 +- experiments/analytic-engine/src/main.rs | 5 - experiments/analytic-engine/src/printer.rs | 412 ++++++++++++++++-- 4 files changed, 419 insertions(+), 63 deletions(-) delete mode 100644 experiments/analytic-engine/src/main.rs diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index 60040b8e..602ea24c 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -29,19 +29,35 @@ pub enum NumRep { }, } +impl NumRep { + pub const fn to_static_str(self) -> &'static str { + match self { + NumRep::Auto => "?", + NumRep::Concrete { is_signed, bit_width } => { + if is_signed { + match bit_width { + BitWidth::Bits8 => "i8", + BitWidth::Bits16 => "i16", + BitWidth::Bits32 => "i32", + BitWidth::Bits64 => "i64", + } + } else { + match bit_width { + BitWidth::Bits8 => "u8", + BitWidth::Bits16 => "u16", + BitWidth::Bits32 => "u32", + BitWidth::Bits64 => "u64", + } + } + } + } + } +} + + impl std::fmt::Display for NumRep { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - NumRep::U8 => write!(f, "u8",), - NumRep::U16 => write!(f, "u16"), - NumRep::U32 => write!(f, "u32"), - NumRep::U64 => write!(f, "u64"), - NumRep::I8 => write!(f, "i8"), - NumRep::I16 => write!(f, "i16"), - NumRep::I32 => write!(f, "i32"), - NumRep::I64 => write!(f, "i64"), - NumRep::AUTO => write!(f, "?"), - } + write!(f, "{}", self.to_static_str()) } } diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs index 505a9d64..4dfd1424 100644 --- a/experiments/analytic-engine/src/elaborator.rs +++ b/experiments/analytic-engine/src/elaborator.rs @@ -52,6 +52,14 @@ impl TryFrom for PrimInt { } } +impl From for NumRep { + fn from(value: IntType) -> Self { + match value { + IntType::Prim(prim) => NumRep::from(prim), + } + } +} + impl From for NumRep { fn from(value: PrimInt) -> Self { match value { @@ -72,14 +80,27 @@ pub(crate) enum IntType { Prim(PrimInt), } -impl std::fmt::Display for IntType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl IntType { + pub const fn to_static_str(self) -> &'static str { match self { - IntType::Prim(prim_int) => write!(f, "{:?}", prim_int), + IntType::Prim(PrimInt::U8) => "u8", + IntType::Prim(PrimInt::U16) => "u16", + IntType::Prim(PrimInt::U32) => "u32", + IntType::Prim(PrimInt::U64) => "u64", + IntType::Prim(PrimInt::I8) => "i8", + IntType::Prim(PrimInt::I16) => "i16", + IntType::Prim(PrimInt::I32) => "i32", + IntType::Prim(PrimInt::I64) => "i64", } } } +impl std::fmt::Display for IntType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_static_str()) + } +} + #[derive(Clone, Debug)] pub(crate) enum TypedExpr { ElabConst(TypeRep, TypedConst), diff --git a/experiments/analytic-engine/src/main.rs b/experiments/analytic-engine/src/main.rs deleted file mode 100644 index c09db116..00000000 --- a/experiments/analytic-engine/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -use analytic_engine::*; - -fn main() { - println!("Hello, world!"); -} diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index 02b4654e..d79f6ee4 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -1,8 +1,200 @@ -use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, Expr, UnaryOp}; +use std::borrow::Cow; +use std::rc::Rc; + +use fragment::Fragment; +use precedence::{cond_paren, Precedence}; + +use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, Expr, NumRep, TypedConst, UnaryOp}; use crate::elaborator::inference::InferenceEngine; use crate::elaborator::{Elaborator, IntType, TypedBinOp, TypedExpr, TypedUnaryOp}; -fn show_bin_op(bin_op: &BinOp) -> String { +pub(crate) mod fragment { + use std::borrow::Cow; + use std::fmt::Write as _; + use std::rc::Rc; + + + + #[derive(Clone, Default)] + pub enum Fragment { + #[default] + Empty, + Char(char), + String(Cow<'static, str>), + DisplayAtom(Rc), + Cat(Box, Box), + } + + impl std::fmt::Debug for Fragment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Empty => write!(f, "Empty"), + Self::Char(c) => f.debug_tuple("Char").field(c).finish(), + Self::String(s) => f.debug_tuple("String").field(s).finish(), + Self::DisplayAtom(at) => f + .debug_tuple("DisplayAtom") + .field(&format!("{}", at)) + .finish(), + Self::Cat(x, y) => f.debug_tuple("Cat").field(x).field(y).finish(), + } + } + } + + impl Fragment { + pub fn cat(self, frag1: Self) -> Self { + Self::Cat(Box::new(self), Box::new(frag1)) + } + + pub fn delimit(self, before: Self, after: Self) -> Self { + Self::cat(before, self).cat(after) + } + + } + + impl std::fmt::Display for Fragment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Fragment::Empty => Ok(()), + Fragment::Char(c) => f.write_char(*c), + Fragment::String(s) => f.write_str(s.as_ref()), + Fragment::DisplayAtom(atom) => std::fmt::Display::fmt(&atom, f), + Fragment::Cat(frag0, frag1) => { + frag0.fmt(f)?; + frag1.fmt(f) + } + } + } + } +} + +pub(crate) mod precedence { + use super::fragment::Fragment; + + #[derive(Copy, Clone, Debug, Default)] + pub(crate) enum Precedence { + /// Highest precedence, as if implicitly (if not actually) parenthesized + #[allow(dead_code)] + Atomic, + /// Highest natural precedence - used for unary operations and type-casts + Mono(MonoLevel), + /// Infix arithmetic operation of the designated arithmetic sub-precedence + Arith(ArithLevel), + /// Lowest natural precedence - used when no particular precedence is required or known #[default] + #[default] + Top + } + + #[derive(Copy, Clone, Debug)] + pub(crate) enum MonoLevel { + // AbsVal and Negate + Prefix = 0, + // Standalone type-casts + Postfix, + } + + #[derive(Copy, Clone, Debug)] + pub(crate) enum ArithLevel { + DivRem = 0, // Highest arithmetic Precedence + Mul, + AddSub, + } + + #[derive(Clone, Copy, PartialEq, Eq, Debug)] + pub(crate) enum Relation { + /// `.<` + Inferior, + /// `.=` + Congruent, + /// `.>` + Superior, + /// `><` + Disjoint, + } + + pub(crate) trait IntransitiveOrd { + fn relate(&self, other: &Self) -> Relation; + } + + impl IntransitiveOrd for MonoLevel { + fn relate(&self, other: &Self) -> Relation { + match (self, other) { + (Self::Prefix, Self::Prefix) | (Self::Postfix, Self::Postfix) => Relation::Congruent, + (Self::Prefix, Self::Postfix) => Relation::Superior, + (Self::Postfix, Self::Prefix) => Relation::Inferior, + } + } + } + + impl IntransitiveOrd for ArithLevel { + fn relate(&self, other: &Self) -> Relation { + match (self, other) { + (Self::DivRem, Self::DivRem) + | (Self::Mul, Self::Mul) + | (Self::AddSub, Self::AddSub) => Relation::Congruent, + (Self::DivRem, Self::Mul) | (Self::Mul, Self::DivRem) => Relation::Disjoint, + (Self::AddSub, _) => Relation::Inferior, + (_, Self::AddSub) => Relation::Superior, + } + } + } + + + /// Rules: + /// x .= x + /// Atomic .> Mono .> *Infix .> Top + /// rel(x, y) = rel(ArithInfix(x), ArithInfix(y)) + /// rel(x, y) = rel(BitwiseInfix(x), BitwiseInfix(y)) + /// rel(x, y) = rel(Calculus(x), Calculus(y)) + /// Bitwise(_) >< Arith(_) + impl IntransitiveOrd for Precedence { + fn relate(&self, other: &Self) -> Relation { + match (self, other) { + // Trivial Congruences + (Precedence::Atomic, Precedence::Atomic) => Relation::Congruent, + (Precedence::Top, Precedence::Top) => Relation::Congruent, + + // Descending relations + (Precedence::Atomic, _) => Relation::Superior, + (_, Precedence::Atomic) => Relation::Superior, + + // Ascending relations + (Precedence::Top, _) => Relation::Inferior, + (_, Precedence::Top) => Relation::Superior, + + // Implications + (Precedence::Mono(x), Precedence::Mono(y)) => x.relate(y), + (Precedence::Arith(x), Precedence::Arith(y)) => x.relate(y), + + // Mixed Relations + (Precedence::Mono(_), Precedence::Arith(_)) => Relation::Superior, + (Precedence::Arith(_), Precedence::Mono(_)) => Relation::Inferior, + } + } + } + + impl Precedence { + pub(crate) const TOP: Self = Precedence::Top; + pub(crate) const DIVREM: Self = Precedence::Arith(ArithLevel::DivRem); + pub(crate) const MUL: Self = Precedence::Arith(ArithLevel::Mul); + pub(crate) const ADDSUB: Self = Precedence::Arith(ArithLevel::AddSub); + pub(crate) const ABSNEG: Self = Precedence::Mono(MonoLevel::Prefix); + pub(crate) const CAST: Self = Precedence::Mono(MonoLevel::Postfix); + + #[allow(dead_code)] + pub(crate) const ATOM: Self = Precedence::Atomic; + } + + pub(crate) fn cond_paren(frag: Fragment, current: Precedence, cutoff: Precedence) -> Fragment { + match current.relate(&cutoff) { + Relation::Disjoint | Relation::Superior => { + frag.delimit(Fragment::Char('('), Fragment::Char(')')) + } + Relation::Congruent | Relation::Inferior => frag + } + } +} + +fn compile_bin_op(bin_op: &BinOp) -> Fragment { let token = match bin_op.get_op() { BasicBinOp::Add => "+", BasicBinOp::Sub => "-", @@ -11,13 +203,16 @@ fn show_bin_op(bin_op: &BinOp) -> String { BasicBinOp::Rem => "%", }; if let Some(rep) = bin_op.cast_rep() { - format!("{}{}", token, rep) + Fragment::cat( + Fragment::String(Cow::Borrowed(token)), + Fragment::String(Cow::Borrowed(rep.to_static_str())), + ) } else { - format!("{}", token) + Fragment::String(Cow::Borrowed(token)) } } -fn show_typed_bin_op(bin_op: &TypedBinOp) -> String { +fn compile_typed_bin_op(bin_op: &TypedBinOp) -> Fragment { let token = match bin_op.inner.get_op() { BasicBinOp::Add => "+", BasicBinOp::Sub => "-", @@ -25,78 +220,207 @@ fn show_typed_bin_op(bin_op: &TypedBinOp) -> String { BasicBinOp::Div => "/", BasicBinOp::Rem => "%", }; - format!( - "({} : ({},{}) -> {})", - token, bin_op.sig.0 .0, bin_op.sig.0 .1, bin_op.sig.1 - ) + let ((t_in_l, t_in_r), t_out) = bin_op.sig; + if t_in_l == t_in_r && t_in_l == t_out { + // If the input types and output type all agree, render as operator only + Fragment::String(Cow::Borrowed(token)) + } else { + Fragment::cat( + Fragment::String(Cow::Borrowed(token)), + Fragment::String(Cow::Owned(format!("@({},{})->{}", t_in_l, t_in_r, t_out))), + ) + }.delimit(Fragment::Char(' '), Fragment::Char(' ')) } -fn show_typed_unary_op(unary_op: &TypedUnaryOp) -> String { +fn compile_typed_unary_op(unary_op: &'_ TypedUnaryOp) -> Fragment { let token = match unary_op.inner.get_op() { BasicUnaryOp::Negate => "~", BasicUnaryOp::AbsVal => "abs", }; - format!("({} : {} -> {})", token, unary_op.sig.0, unary_op.sig.1) + let (t_in, t_out) = unary_op.sig; + if t_in == t_out { + Fragment::String(Cow::Borrowed(token)) + } else { + Fragment::cat( + Fragment::String(Cow::Borrowed(token)), + Fragment::String(Cow::Owned(format!("@{}->{}", t_in, t_out))), + ) + } } -fn show_unary_op(unary_op: &UnaryOp) -> String { +fn compile_prefix(op: &UnaryOp, inner: &Expr, inner_prec: Precedence) -> Fragment { + Fragment::cat(compile_unary_op(op), compile_expr(inner, inner_prec)) +} + +fn compile_postfix<'a>(token: &'static str, rep: NumRep, inner: &Expr, inner_prec: Precedence) -> Fragment { + Fragment::cat(compile_expr(inner, inner_prec), Fragment::cat(Fragment::String(Cow::Borrowed(token)), Fragment::String(Cow::Borrowed(rep.to_static_str())))) +} + + +fn compile_unary_op<'a>(unary_op: &UnaryOp) -> Fragment { let token = match unary_op.get_op() { BasicUnaryOp::Negate => "~", BasicUnaryOp::AbsVal => "abs", }; if let Some(rep) = unary_op.cast_rep() { - format!("{}{}", token, rep) + Fragment::cat( + Fragment::String(Cow::Borrowed(token)), + Fragment::String(Cow::Borrowed(rep.to_static_str())), + ) } else { - format!("{}", token) + Fragment::String(Cow::Borrowed(token)) } } + +fn compile_binop(op: BinOp, lhs: &Expr, rhs: &Expr, lhs_prec: Precedence, rhs_prec: Precedence) -> Fragment { + Fragment::delimit(compile_bin_op(&op), compile_expr(lhs, lhs_prec), compile_expr(rhs, rhs_prec)) +} + // FIXME - adopt pretty-printing engine with precedence rules -pub fn show_expr(expr: &Expr) -> String { +fn compile_expr(expr: &Expr, prec: Precedence) -> Fragment { match expr { - Expr::Const(typed_const) => format!("{}", typed_const), - Expr::BinOp(bin_op, expr, expr1) => format!( - "({} {} {})", - show_expr(expr), - show_bin_op(bin_op), - show_expr(expr1) - ), - Expr::UnaryOp(unary_op, expr) => { - format!("{}({})", show_unary_op(unary_op), show_expr(expr)) + Expr::Const(typed_const) => Fragment::DisplayAtom(Rc::new(typed_const.clone())), + Expr::BinOp(op, lhs, rhs) => match op.get_op() { + BasicBinOp::Add | BasicBinOp::Sub => + cond_paren( + compile_binop(*op, lhs, rhs, Precedence::ADDSUB, Precedence::ADDSUB), + prec, + Precedence::ADDSUB, + ), + BasicBinOp::Mul => cond_paren( + compile_binop(*op, lhs, rhs, Precedence::MUL, Precedence::MUL), + prec, + Precedence::MUL, + ), + BasicBinOp::Div | BasicBinOp::Rem => cond_paren( + compile_binop(*op, lhs, rhs, Precedence::DIVREM, Precedence::DIVREM), + prec, + Precedence::DIVREM, + ), } - Expr::Cast(num_rep, expr) => format!("{} as {}", show_expr(expr), num_rep), + Expr::UnaryOp(unary_op, expr) => + cond_paren( + compile_prefix(unary_op, expr, Precedence::ABSNEG), + prec, + Precedence::ABSNEG, + ), + Expr::Cast(num_rep, expr) => cond_paren( + compile_postfix(" as ", *num_rep, expr, Precedence::CAST), + prec, + Precedence::CAST, + ), + } +} + +fn show_expr(expr: &Expr) -> String { + format!("{}", compile_expr(expr, Precedence::TOP)) +} + +fn compile_elab_const(t: IntType, typed_const: &TypedConst) -> Fragment { + if NumRep::from(t) == typed_const.get_rep() { + // If the int-type is directly analogous to the original rep, omit the type-annotation + Fragment::DisplayAtom(Rc::new(typed_const.clone())) + } else { + Fragment::cat( + Fragment::DisplayAtom(Rc::new(typed_const.clone())), + Fragment::cat(Fragment::Char('@'), Fragment::String(Cow::Borrowed(t.to_static_str()))) + + ) } } +fn compile_elab_binop(t: IntType, op: &TypedBinOp, lhs: &TypedExpr, rhs: &TypedExpr, lhs_prec: Precedence, rhs_prec: Precedence) -> Fragment { + compile_typed_expr(lhs, lhs_prec) + .cat(compile_typed_bin_op(op)) + .cat(compile_typed_expr(rhs, rhs_prec)) + .cat(Fragment::String(Cow::Borrowed(" :: "))) + .cat(Fragment::String(Cow::Borrowed(t.to_static_str()))) +} + +fn compile_elab_prefix(t: IntType, op: &TypedUnaryOp, inner: &TypedExpr, inner_prec: Precedence) -> Fragment { + if t == op.sig.0 && op.sig.0 == op.sig.1 { + compile_typed_unary_op(op) + .cat(compile_typed_expr(inner, inner_prec)) + } else { + compile_typed_unary_op(op) + .cat(compile_typed_expr(inner, inner_prec)) + .cat(Fragment::String(Cow::Borrowed(" :: "))) + .cat(Fragment::String(Cow::Borrowed(t.to_static_str()))) + } +} + +fn compile_elab_postfix<'a>(t: IntType, rep: NumRep, inner: &TypedExpr, inner_prec: Precedence) -> Fragment { + if NumRep::from(t) == rep { + // If the int-type is directly analogous to the original rep, omit the type-annotation + compile_typed_expr(inner, inner_prec) + .cat(Fragment::String(Cow::Borrowed(" as "))) + .cat(Fragment::String(Cow::Borrowed(rep.to_static_str()))) + } else { + // NOTE - this would normally be a panic but we are content to observe it without failing + eprintln!("[WARNING]: Postfix operation (assumed to be Cast) has mismatched NumRep and IntType..."); + compile_typed_expr(inner, inner_prec) + .cat(Fragment::String(Cow::Borrowed(" as "))) + .cat(Fragment::String(Cow::Borrowed(rep.to_static_str()))) + .cat(Fragment::String(Cow::Borrowed(" :: "))) + .cat(Fragment::String(Cow::Borrowed(t.to_static_str()))) + } +} + + // FIXME - adopt pretty-printing engine with precedence rules -fn show_typed_expr(t_expr: &TypedExpr) -> String { +fn compile_typed_expr(t_expr: &TypedExpr, prec: Precedence) -> Fragment { match t_expr { - TypedExpr::ElabConst(t, typed_const) => { - format!("({}: {})", typed_const, t) + TypedExpr::ElabConst(t, typed_const) => compile_elab_const(*t, typed_const), + TypedExpr::ElabBinOp(t, typed_bin_op, lhs, rhs) => match typed_bin_op.inner.get_op() { + BasicBinOp::Add | BasicBinOp::Sub => + cond_paren( + compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::ADDSUB, Precedence::ADDSUB), + prec, + Precedence::ADDSUB, + ), + BasicBinOp::Mul => cond_paren( + compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::MUL, Precedence::MUL), + prec, + Precedence::MUL, + ), + BasicBinOp::Div | BasicBinOp::Rem => cond_paren( + compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::DIVREM, Precedence::DIVREM), + prec, + Precedence::DIVREM, + ), } - TypedExpr::ElabBinOp(t, typed_bin_op, typed_expr, typed_expr1) => { - format!( - "({} {} {} : {})", - show_typed_expr(typed_expr), - show_typed_bin_op(typed_bin_op), - show_typed_expr(typed_expr1), - t + TypedExpr::ElabUnaryOp(t, typed_unary_op, inner) => { + cond_paren( + compile_elab_prefix( + *t, + typed_unary_op, + inner, + Precedence::ABSNEG, + ), + prec, + Precedence::ABSNEG ) } - TypedExpr::ElabUnaryOp(t, typed_unary_op, typed_expr) => { - format!( - "({}({}) : {})", - show_typed_unary_op(typed_unary_op), - show_typed_expr(typed_expr), - t + TypedExpr::ElabCast(t, num_rep, inner) => { + cond_paren( + compile_elab_postfix( + *t, + *num_rep, + inner, + Precedence::CAST, + ), + prec, + Precedence::CAST, ) } - TypedExpr::ElabCast(t, num_rep, typed_expr) => { - format!("({} as {} : {})", show_typed_expr(typed_expr), num_rep, t) - } } } +fn show_typed_expr(expr: &TypedExpr) -> String { + format!("{}", compile_typed_expr(expr, Precedence::TOP)) +} + pub fn print_conversion(expr: &Expr) { let mut ie = InferenceEngine::new(); match ie.infer_var_expr(expr) { From 88b1df69773f30d4441a7e28e1c1f2abe3a50721 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 4 Nov 2024 13:43:29 +1100 Subject: [PATCH 08/20] Add intervening space between unary op and arg --- experiments/analytic-engine/src/printer.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index d79f6ee4..33873eab 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -241,11 +241,9 @@ fn compile_typed_unary_op(unary_op: &'_ TypedUnaryOp) -> Fragment { if t_in == t_out { Fragment::String(Cow::Borrowed(token)) } else { - Fragment::cat( - Fragment::String(Cow::Borrowed(token)), - Fragment::String(Cow::Owned(format!("@{}->{}", t_in, t_out))), - ) - } + Fragment::String(Cow::Borrowed(token)) + .cat(Fragment::String(Cow::Owned(format!("@{}->{}", t_in, t_out)))) + }.cat(Fragment::Char(' ')) } fn compile_prefix(op: &UnaryOp, inner: &Expr, inner_prec: Precedence) -> Fragment { From 065c7a0de6be1d8fd69af90e5d8e916aca8f8455 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Mon, 4 Nov 2024 13:46:19 +1100 Subject: [PATCH 09/20] Add spaces around raw binop in printing --- experiments/analytic-engine/src/printer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index 33873eab..7cf3980a 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -209,7 +209,7 @@ fn compile_bin_op(bin_op: &BinOp) -> Fragment { ) } else { Fragment::String(Cow::Borrowed(token)) - } + }.delimit(Fragment::Char(' '), Fragment::Char(' ')) } fn compile_typed_bin_op(bin_op: &TypedBinOp) -> Fragment { From 6c7253bfa58664c2ef777809dc0f21fae1585163 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Wed, 6 Nov 2024 19:49:18 +1100 Subject: [PATCH 10/20] Add eval model for concrete machine-int functions --- experiments/analytic-engine/src/core.rs | 6 +- experiments/analytic-engine/src/eval.rs | 268 +++++++++++++++++++++ experiments/analytic-engine/src/lib.rs | 1 + experiments/analytic-engine/src/printer.rs | 166 ++++++++----- 4 files changed, 374 insertions(+), 67 deletions(-) create mode 100644 experiments/analytic-engine/src/eval.rs diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index 602ea24c..b5ebdbab 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -33,7 +33,10 @@ impl NumRep { pub const fn to_static_str(self) -> &'static str { match self { NumRep::Auto => "?", - NumRep::Concrete { is_signed, bit_width } => { + NumRep::Concrete { + is_signed, + bit_width, + } => { if is_signed { match bit_width { BitWidth::Bits8 => "i8", @@ -54,7 +57,6 @@ impl NumRep { } } - impl std::fmt::Display for NumRep { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.to_static_str()) diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs new file mode 100644 index 00000000..f9790d7a --- /dev/null +++ b/experiments/analytic-engine/src/eval.rs @@ -0,0 +1,268 @@ +use num_bigint::{BigInt, TryFromBigIntError}; +use num_traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub, SaturatingAdd, SaturatingMul, + SaturatingSub, WrappingAdd, WrappingMul, WrappingSub, +}; +use std::any::type_name; +use std::cell::LazyCell; +use std::ops::{Add, Mul, Sub}; +use std::rc::Rc; + +// SECTION - Evaluation model for potentially heterogenous machine integer operations +#[derive(Clone, Debug)] +pub enum Eval { + NaN, + Direct(T), + Indirect(IndirectEval), +} + +impl Eval { + pub const fn is_nan(&self) -> bool { + matches!(self, &Eval::NaN) + } +} + +impl PartialEq for Eval +where + T: PartialEq + Copy, + BigInt: From, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::NaN, _) | (_, Self::NaN) => false, + (Self::Direct(l0), Self::Direct(r0)) => l0 == r0, + (Self::Indirect(l0), Self::Indirect(r0)) => &**l0.value == &**r0.value, + (Self::Direct(n), Self::Indirect(IndirectEval { ref value, .. })) + | (Self::Indirect(IndirectEval { ref value, .. }), Self::Direct(n)) => { + &***value == &BigInt::from(*n) + } + } + } +} + +#[derive(Clone)] +pub struct IndirectEval { + value: Rc BigInt>>>, + saturated: T, + wrapped: T, +} + +impl std::fmt::Debug for IndirectEval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IndirectEval") + .field("value", &self.value) + .field("saturated", &self.saturated) + .field("wrapped", &self.wrapped) + .finish() + } +} +// !SECTION + +/// Macro for bulk definition of homogenously typed binary operations with a checked version provided by a trait (`num_traits`), where +/// `None`` indicates underflow or overflow, falling back on available saturating and wrapping variants of the same operation for indirect computations +macro_rules! homogenous { + ( $( $tr:ident, $meth:ident, $sat_tr:ident, $sat_meth:ident, $wrap_tr:ident, $wrap_meth:ident => $( ( $fname:ident , $t:ty ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + pub fn $fname(lhs: $t, rhs: $t) -> Eval<$t> { + match <$t as $tr>::$meth(&lhs, &rhs) { + Some(res) => Eval::Direct(res), + None => { + Eval::Indirect( + IndirectEval { + value: Rc::new(LazyCell::new( + Box::new(move || + BigInt::from(lhs).$meth(&BigInt::from(rhs)).unwrap() + ) + )), + saturated: <$t as $sat_tr>::$sat_meth(&lhs, &rhs), + wrapped: <$t as $wrap_tr>::$wrap_meth(&lhs, &rhs), + } + ) + } + } + } + )* + )* + }; +} + +/// Macro for bulk definition of homogenously typed binary operations with a quotient version provided by a trait (`num_traits`), where None indicates NaN +macro_rules! homogenous_quotient { + ( $( $tr:ident, $meth:ident => $( ( $fname:ident, $t:ty ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + pub fn $fname(lhs: $t, rhs: $t) -> Eval<$t> { + match <$t as $tr>::$meth(&lhs, &rhs) { + Some(res) => Eval::Direct(res), + None => Eval::NaN, + } + } + )* + )* + } +} + +/// Macro for bulk definition of homogenous source-type typed binary operations where the output type can represent every possible input type, but the computation +macro_rules! widening { + ( $( $tr:ident, $meth:ident => $( ( $in_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + $( + pub fn $fname(lhs: $in_t, rhs: $in_t) -> Eval<$out_t> { + Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) + } + )* + )* + )* + }; +} + +/// Macro for bulk definition of homogenous source-type typed binary operations where the output type can represent every possible input type, but the nominal +macro_rules! widening { + ( $( $tr:ident, $meth:ident => $( ( $in_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + $( + pub fn $fname(lhs: $in_t, rhs: $in_t) -> Eval<$out_t> { + Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) + } + )* + )* + )* + }; +} + +// SECTION - Combinatorial Explosion + +// SECTION - strictly homogenous (T -> T -> T) binary operations +homogenous! { + CheckedAdd, checked_add, SaturatingAdd, saturating_add, WrappingAdd, wrapping_add => + (add_u8, u8), + (add_u16, u16), + (add_u32, u32), + (add_u64, u64), + (add_i8, i8), + (add_i16, i16), + (add_i32, i32), + (add_i64, i64); + CheckedSub, checked_sub, SaturatingSub, saturating_sub, WrappingSub, wrapping_sub => + (sub_u8, u8), + (sub_u16, u16), + (sub_u32, u32), + (sub_u64, u64), + (sub_i8, i8), + (sub_i16, i16), + (sub_i32, i32), + (sub_i64, i64); + CheckedMul, checked_mul, SaturatingMul, saturating_mul, WrappingMul, wrapping_mul => + (mul_u8, u8), + (mul_u16, u16), + (mul_u32, u32), + (mul_u64, u64), + (mul_i8, i8), + (mul_i16, i16), + (mul_i32, i32), + (mul_i64, i64); +} + +homogenous_quotient! { + CheckedDiv, checked_div => + (div_u8, u8), + (div_u16, u16), + (div_u32, u32), + (div_u64, u64), + (div_i8, i8), + (div_i16, i16), + (div_i32, i32), + (div_i64, i64); + CheckedRem, checked_rem => + (rem_u8, u8), + (rem_u16, u16), + (rem_u32, u32), + (rem_u64, u64), + (rem_i8, i8), + (rem_i16, i16), + (rem_i32, i32), + (rem_i64, i64); +} +// !SECTION + +widening! { + Add, add => + (u8 => (add_u8_u16, u16), (add_u8_u32, u32), (add_u8_u64, u64), (add_u8_i16, i16), (add_u8_i32, i32), (add_u8_i64, i64)), + (u16 => (add_u16_u32, u32), (add_u16_u64, u64), (add_u16_i32, i32), (add_u16_i64, i64)), + (u32 => (add_u32_u64, u64), (add_u32_i64, i64)), + (i8 => (add_i8_i16, i16), (add_i8_i32, i32), (add_i8_i64, i64)); + Sub, sub => + (u8 => (sub_u8_i16, i16), (sub_u8_i32, i32), (sub_u8_i64, i64)), + (u16 => (sub_u16_i32, i32), (sub_u16_i64, i64)), + (u32 => (sub_u32_i64, i64)), + (i8 => (sub_i8_i16, i16), (sub_i8_i32, i32), (sub_i8_i64, i64)), + (i16 => (sub_i16_i32, i32), (sub_i16_i64, i64)), + (i32 => (sub_i32_i64, i64)); + Mul, mul => + (u8 => (mul_u8_u16, u16), (mul_u8_u32, u32), (mul_u8_u64, u64), (mul_u8_i32, i32), (mul_u8_i64, i64)), + (u16 => (mul_u16_u32, u32), (mul_u16_u64, u64), (mul_u16_i64, i64)), + (u32 => (mul_u32_u64, u64)), + (i8 => (mul_i8_i16, i16), (mul_i8_i32, i32), (mul_i8_i64, i64)), + (i16 => (mul_i16_i32, i32), (mul_i16_i64, i64)), + (i32 => (mul_i32_i64, i64)); +} + +// !SECTION + +pub fn eval_fallback( + lhs: Lhs, + rhs: Rhs, + _op_hint: &'static str, + checked_op: impl FnOnce(&BigInt, &BigInt) -> Option, + err_none: impl FnOnce() -> E, +) -> Result +where + BigInt: From + From, + Res: TryFrom>, + E: From>, +{ + eprintln!("[INFO]: encountered fallback operation `{_op_hint} : (({}, {}) -> {})` that may benefit from standalone function", + type_name::(), type_name::(), type_name::() + ); + let big_l = BigInt::from(lhs); + let big_r = BigInt::from(rhs); + let o_big_res = checked_op(&big_l, &big_r); + if let Some(big_res) = o_big_res { + Ok(>::try_from(big_res)?) + } else { + Err(err_none()) + } +} + +#[derive(Debug)] +pub enum EvalError { + Downcast(TryFromBigIntError), + BadOperation, +} + +impl std::fmt::Display for EvalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvalError::Downcast(e) => write!(f, "failed to cast from BigInt: {e}"), + EvalError::BadOperation => write!(f, "bad operation (division or remainder by zero)"), + } + } +} + +impl std::error::Error for EvalError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + EvalError::Downcast(e) => Some(e), + EvalError::BadOperation => None, + } + } +} + +impl From> for EvalError { + fn from(value: TryFromBigIntError) -> EvalError { + Self::Downcast(value) + } +} diff --git a/experiments/analytic-engine/src/lib.rs b/experiments/analytic-engine/src/lib.rs index 7c58aca7..cc327bf1 100644 --- a/experiments/analytic-engine/src/lib.rs +++ b/experiments/analytic-engine/src/lib.rs @@ -1,3 +1,4 @@ pub mod core; pub mod elaborator; +pub mod eval; pub mod printer; diff --git a/experiments/analytic-engine/src/printer.rs b/experiments/analytic-engine/src/printer.rs index 7cf3980a..2b93731d 100644 --- a/experiments/analytic-engine/src/printer.rs +++ b/experiments/analytic-engine/src/printer.rs @@ -13,8 +13,6 @@ pub(crate) mod fragment { use std::fmt::Write as _; use std::rc::Rc; - - #[derive(Clone, Default)] pub enum Fragment { #[default] @@ -48,7 +46,6 @@ pub(crate) mod fragment { pub fn delimit(self, before: Self, after: Self) -> Self { Self::cat(before, self).cat(after) } - } impl std::fmt::Display for Fragment { @@ -81,7 +78,7 @@ pub(crate) mod precedence { Arith(ArithLevel), /// Lowest natural precedence - used when no particular precedence is required or known #[default] #[default] - Top + Top, } #[derive(Copy, Clone, Debug)] @@ -118,7 +115,9 @@ pub(crate) mod precedence { impl IntransitiveOrd for MonoLevel { fn relate(&self, other: &Self) -> Relation { match (self, other) { - (Self::Prefix, Self::Prefix) | (Self::Postfix, Self::Postfix) => Relation::Congruent, + (Self::Prefix, Self::Prefix) | (Self::Postfix, Self::Postfix) => { + Relation::Congruent + } (Self::Prefix, Self::Postfix) => Relation::Superior, (Self::Postfix, Self::Prefix) => Relation::Inferior, } @@ -138,7 +137,6 @@ pub(crate) mod precedence { } } - /// Rules: /// x .= x /// Atomic .> Mono .> *Infix .> Top @@ -189,7 +187,7 @@ pub(crate) mod precedence { Relation::Disjoint | Relation::Superior => { frag.delimit(Fragment::Char('('), Fragment::Char(')')) } - Relation::Congruent | Relation::Inferior => frag + Relation::Congruent | Relation::Inferior => frag, } } } @@ -209,7 +207,8 @@ fn compile_bin_op(bin_op: &BinOp) -> Fragment { ) } else { Fragment::String(Cow::Borrowed(token)) - }.delimit(Fragment::Char(' '), Fragment::Char(' ')) + } + .delimit(Fragment::Char(' '), Fragment::Char(' ')) } fn compile_typed_bin_op(bin_op: &TypedBinOp) -> Fragment { @@ -229,7 +228,8 @@ fn compile_typed_bin_op(bin_op: &TypedBinOp) -> Fragment { Fragment::String(Cow::Borrowed(token)), Fragment::String(Cow::Owned(format!("@({},{})->{}", t_in_l, t_in_r, t_out))), ) - }.delimit(Fragment::Char(' '), Fragment::Char(' ')) + } + .delimit(Fragment::Char(' '), Fragment::Char(' ')) } fn compile_typed_unary_op(unary_op: &'_ TypedUnaryOp) -> Fragment { @@ -241,20 +241,33 @@ fn compile_typed_unary_op(unary_op: &'_ TypedUnaryOp) -> Fragment { if t_in == t_out { Fragment::String(Cow::Borrowed(token)) } else { - Fragment::String(Cow::Borrowed(token)) - .cat(Fragment::String(Cow::Owned(format!("@{}->{}", t_in, t_out)))) - }.cat(Fragment::Char(' ')) + Fragment::String(Cow::Borrowed(token)).cat(Fragment::String(Cow::Owned(format!( + "@{}->{}", + t_in, t_out + )))) + } + .cat(Fragment::Char(' ')) } fn compile_prefix(op: &UnaryOp, inner: &Expr, inner_prec: Precedence) -> Fragment { Fragment::cat(compile_unary_op(op), compile_expr(inner, inner_prec)) } -fn compile_postfix<'a>(token: &'static str, rep: NumRep, inner: &Expr, inner_prec: Precedence) -> Fragment { - Fragment::cat(compile_expr(inner, inner_prec), Fragment::cat(Fragment::String(Cow::Borrowed(token)), Fragment::String(Cow::Borrowed(rep.to_static_str())))) +fn compile_postfix<'a>( + token: &'static str, + rep: NumRep, + inner: &Expr, + inner_prec: Precedence, +) -> Fragment { + Fragment::cat( + compile_expr(inner, inner_prec), + Fragment::cat( + Fragment::String(Cow::Borrowed(token)), + Fragment::String(Cow::Borrowed(rep.to_static_str())), + ), + ) } - fn compile_unary_op<'a>(unary_op: &UnaryOp) -> Fragment { let token = match unary_op.get_op() { BasicUnaryOp::Negate => "~", @@ -270,9 +283,18 @@ fn compile_unary_op<'a>(unary_op: &UnaryOp) -> Fragment { } } - -fn compile_binop(op: BinOp, lhs: &Expr, rhs: &Expr, lhs_prec: Precedence, rhs_prec: Precedence) -> Fragment { - Fragment::delimit(compile_bin_op(&op), compile_expr(lhs, lhs_prec), compile_expr(rhs, rhs_prec)) +fn compile_binop( + op: BinOp, + lhs: &Expr, + rhs: &Expr, + lhs_prec: Precedence, + rhs_prec: Precedence, +) -> Fragment { + Fragment::delimit( + compile_bin_op(&op), + compile_expr(lhs, lhs_prec), + compile_expr(rhs, rhs_prec), + ) } // FIXME - adopt pretty-printing engine with precedence rules @@ -280,8 +302,7 @@ fn compile_expr(expr: &Expr, prec: Precedence) -> Fragment { match expr { Expr::Const(typed_const) => Fragment::DisplayAtom(Rc::new(typed_const.clone())), Expr::BinOp(op, lhs, rhs) => match op.get_op() { - BasicBinOp::Add | BasicBinOp::Sub => - cond_paren( + BasicBinOp::Add | BasicBinOp::Sub => cond_paren( compile_binop(*op, lhs, rhs, Precedence::ADDSUB, Precedence::ADDSUB), prec, Precedence::ADDSUB, @@ -296,13 +317,12 @@ fn compile_expr(expr: &Expr, prec: Precedence) -> Fragment { prec, Precedence::DIVREM, ), - } - Expr::UnaryOp(unary_op, expr) => - cond_paren( - compile_prefix(unary_op, expr, Precedence::ABSNEG), - prec, - Precedence::ABSNEG, - ), + }, + Expr::UnaryOp(unary_op, expr) => cond_paren( + compile_prefix(unary_op, expr, Precedence::ABSNEG), + prec, + Precedence::ABSNEG, + ), Expr::Cast(num_rep, expr) => cond_paren( compile_postfix(" as ", *num_rep, expr, Precedence::CAST), prec, @@ -322,13 +342,22 @@ fn compile_elab_const(t: IntType, typed_const: &TypedConst) -> Fragment { } else { Fragment::cat( Fragment::DisplayAtom(Rc::new(typed_const.clone())), - Fragment::cat(Fragment::Char('@'), Fragment::String(Cow::Borrowed(t.to_static_str()))) - + Fragment::cat( + Fragment::Char('@'), + Fragment::String(Cow::Borrowed(t.to_static_str())), + ), ) } } -fn compile_elab_binop(t: IntType, op: &TypedBinOp, lhs: &TypedExpr, rhs: &TypedExpr, lhs_prec: Precedence, rhs_prec: Precedence) -> Fragment { +fn compile_elab_binop( + t: IntType, + op: &TypedBinOp, + lhs: &TypedExpr, + rhs: &TypedExpr, + lhs_prec: Precedence, + rhs_prec: Precedence, +) -> Fragment { compile_typed_expr(lhs, lhs_prec) .cat(compile_typed_bin_op(op)) .cat(compile_typed_expr(rhs, rhs_prec)) @@ -336,10 +365,14 @@ fn compile_elab_binop(t: IntType, op: &TypedBinOp, lhs: &TypedExpr, inner: &TypedExpr, inner_prec: Precedence) -> Fragment { +fn compile_elab_prefix( + t: IntType, + op: &TypedUnaryOp, + inner: &TypedExpr, + inner_prec: Precedence, +) -> Fragment { if t == op.sig.0 && op.sig.0 == op.sig.1 { - compile_typed_unary_op(op) - .cat(compile_typed_expr(inner, inner_prec)) + compile_typed_unary_op(op).cat(compile_typed_expr(inner, inner_prec)) } else { compile_typed_unary_op(op) .cat(compile_typed_expr(inner, inner_prec)) @@ -348,7 +381,12 @@ fn compile_elab_prefix(t: IntType, op: &TypedUnaryOp, inner: &TypedExpr } } -fn compile_elab_postfix<'a>(t: IntType, rep: NumRep, inner: &TypedExpr, inner_prec: Precedence) -> Fragment { +fn compile_elab_postfix<'a>( + t: IntType, + rep: NumRep, + inner: &TypedExpr, + inner_prec: Precedence, +) -> Fragment { if NumRep::from(t) == rep { // If the int-type is directly analogous to the original rep, omit the type-annotation compile_typed_expr(inner, inner_prec) @@ -365,53 +403,51 @@ fn compile_elab_postfix<'a>(t: IntType, rep: NumRep, inner: &TypedExpr, } } - // FIXME - adopt pretty-printing engine with precedence rules fn compile_typed_expr(t_expr: &TypedExpr, prec: Precedence) -> Fragment { match t_expr { TypedExpr::ElabConst(t, typed_const) => compile_elab_const(*t, typed_const), TypedExpr::ElabBinOp(t, typed_bin_op, lhs, rhs) => match typed_bin_op.inner.get_op() { - BasicBinOp::Add | BasicBinOp::Sub => - cond_paren( - compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::ADDSUB, Precedence::ADDSUB), - prec, + BasicBinOp::Add | BasicBinOp::Sub => cond_paren( + compile_elab_binop( + *t, + typed_bin_op, + lhs, + rhs, + Precedence::ADDSUB, Precedence::ADDSUB, ), + prec, + Precedence::ADDSUB, + ), BasicBinOp::Mul => cond_paren( compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::MUL, Precedence::MUL), prec, Precedence::MUL, ), BasicBinOp::Div | BasicBinOp::Rem => cond_paren( - compile_elab_binop(*t, typed_bin_op, lhs, rhs, Precedence::DIVREM, Precedence::DIVREM), - prec, - Precedence::DIVREM, - ), - } - TypedExpr::ElabUnaryOp(t, typed_unary_op, inner) => { - cond_paren( - compile_elab_prefix( - *t, - typed_unary_op, - inner, - Precedence::ABSNEG, - ), - prec, - Precedence::ABSNEG - ) - } - TypedExpr::ElabCast(t, num_rep, inner) => { - cond_paren( - compile_elab_postfix( + compile_elab_binop( *t, - *num_rep, - inner, - Precedence::CAST, + typed_bin_op, + lhs, + rhs, + Precedence::DIVREM, + Precedence::DIVREM, ), prec, - Precedence::CAST, - ) - } + Precedence::DIVREM, + ), + }, + TypedExpr::ElabUnaryOp(t, typed_unary_op, inner) => cond_paren( + compile_elab_prefix(*t, typed_unary_op, inner, Precedence::ABSNEG), + prec, + Precedence::ABSNEG, + ), + TypedExpr::ElabCast(t, num_rep, inner) => cond_paren( + compile_elab_postfix(*t, *num_rep, inner, Precedence::CAST), + prec, + Precedence::CAST, + ), } } From c2af9fe99a47feb24e6ae9f7b905b3504b960fef Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Wed, 6 Nov 2024 19:58:01 +1100 Subject: [PATCH 11/20] Refine eval model --- experiments/analytic-engine/src/eval.rs | 114 +++++++----------------- 1 file changed, 33 insertions(+), 81 deletions(-) diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs index f9790d7a..0930d3fd 100644 --- a/experiments/analytic-engine/src/eval.rs +++ b/experiments/analytic-engine/src/eval.rs @@ -1,19 +1,29 @@ -use num_bigint::{BigInt, TryFromBigIntError}; -use num_traits::{ - CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub, SaturatingAdd, SaturatingMul, - SaturatingSub, WrappingAdd, WrappingMul, WrappingSub, -}; +use num_bigint::BigInt; +use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub}; use std::any::type_name; use std::cell::LazyCell; use std::ops::{Add, Mul, Sub}; use std::rc::Rc; +#[derive(Clone)] +pub struct IndirectEval { + value: Rc BigInt>>>, +} + +impl std::fmt::Debug for IndirectEval { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IndirectEval") + .field("value", &self.value) + .finish() + } +} + // SECTION - Evaluation model for potentially heterogenous machine integer operations #[derive(Clone, Debug)] pub enum Eval { NaN, Direct(T), - Indirect(IndirectEval), + Indirect(IndirectEval), } impl Eval { @@ -40,28 +50,12 @@ where } } -#[derive(Clone)] -pub struct IndirectEval { - value: Rc BigInt>>>, - saturated: T, - wrapped: T, -} - -impl std::fmt::Debug for IndirectEval { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("IndirectEval") - .field("value", &self.value) - .field("saturated", &self.saturated) - .field("wrapped", &self.wrapped) - .finish() - } -} // !SECTION /// Macro for bulk definition of homogenously typed binary operations with a checked version provided by a trait (`num_traits`), where /// `None`` indicates underflow or overflow, falling back on available saturating and wrapping variants of the same operation for indirect computations macro_rules! homogenous { - ( $( $tr:ident, $meth:ident, $sat_tr:ident, $sat_meth:ident, $wrap_tr:ident, $wrap_meth:ident => $( ( $fname:ident , $t:ty ) ),+ $(,)? );+ $(;)? ) => { + ( $( $tr:ident, $meth:ident => $( ( $fname:ident , $t:ty ) ),+ $(,)? );+ $(;)? ) => { $( $( pub fn $fname(lhs: $t, rhs: $t) -> Eval<$t> { @@ -75,8 +69,6 @@ macro_rules! homogenous { BigInt::from(lhs).$meth(&BigInt::from(rhs)).unwrap() ) )), - saturated: <$t as $sat_tr>::$sat_meth(&lhs, &rhs), - wrapped: <$t as $wrap_tr>::$wrap_meth(&lhs, &rhs), } ) } @@ -103,22 +95,7 @@ macro_rules! homogenous_quotient { } } -/// Macro for bulk definition of homogenous source-type typed binary operations where the output type can represent every possible input type, but the computation -macro_rules! widening { - ( $( $tr:ident, $meth:ident => $( ( $in_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { - $( - $( - $( - pub fn $fname(lhs: $in_t, rhs: $in_t) -> Eval<$out_t> { - Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) - } - )* - )* - )* - }; -} - -/// Macro for bulk definition of homogenous source-type typed binary operations where the output type can represent every possible input type, but the nominal +/// Macro for bulk definition of homogenous source-type typed binary operations where the output type can represent every possible input type, but the computation might not succeed macro_rules! widening { ( $( $tr:ident, $meth:ident => $( ( $in_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { $( @@ -137,7 +114,7 @@ macro_rules! widening { // SECTION - strictly homogenous (T -> T -> T) binary operations homogenous! { - CheckedAdd, checked_add, SaturatingAdd, saturating_add, WrappingAdd, wrapping_add => + CheckedAdd, checked_add => (add_u8, u8), (add_u16, u16), (add_u32, u32), @@ -146,7 +123,7 @@ homogenous! { (add_i16, i16), (add_i32, i32), (add_i64, i64); - CheckedSub, checked_sub, SaturatingSub, saturating_sub, WrappingSub, wrapping_sub => + CheckedSub, checked_sub => (sub_u8, u8), (sub_u16, u16), (sub_u32, u32), @@ -155,7 +132,7 @@ homogenous! { (sub_i16, i16), (sub_i32, i32), (sub_i64, i64); - CheckedMul, checked_mul, SaturatingMul, saturating_mul, WrappingMul, wrapping_mul => + CheckedMul, checked_mul => (mul_u8, u8), (mul_u16, u16), (mul_u32, u32), @@ -212,17 +189,15 @@ widening! { // !SECTION -pub fn eval_fallback( +pub fn eval_fallback( lhs: Lhs, rhs: Rhs, _op_hint: &'static str, checked_op: impl FnOnce(&BigInt, &BigInt) -> Option, - err_none: impl FnOnce() -> E, -) -> Result +) -> Eval where BigInt: From + From, - Res: TryFrom>, - E: From>, + Res: TryFrom, { eprintln!("[INFO]: encountered fallback operation `{_op_hint} : (({}, {}) -> {})` that may benefit from standalone function", type_name::(), type_name::(), type_name::() @@ -231,38 +206,15 @@ where let big_r = BigInt::from(rhs); let o_big_res = checked_op(&big_l, &big_r); if let Some(big_res) = o_big_res { - Ok(>::try_from(big_res)?) - } else { - Err(err_none()) - } -} - -#[derive(Debug)] -pub enum EvalError { - Downcast(TryFromBigIntError), - BadOperation, -} - -impl std::fmt::Display for EvalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EvalError::Downcast(e) => write!(f, "failed to cast from BigInt: {e}"), - EvalError::BadOperation => write!(f, "bad operation (division or remainder by zero)"), - } - } -} - -impl std::error::Error for EvalError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - EvalError::Downcast(e) => Some(e), - EvalError::BadOperation => None, + let _big_res = big_res.clone(); + if let Ok(res) = >::try_from(big_res) { + Eval::Direct(res) + } else { + Eval::Indirect(IndirectEval { + value: Rc::new(LazyCell::new(Box::new(move || _big_res))), + }) } - } -} - -impl From> for EvalError { - fn from(value: TryFromBigIntError) -> EvalError { - Self::Downcast(value) + } else { + Eval::NaN } } From f6bd6990769855f9fe56e2d283b7b3f2a2c83fc4 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Wed, 6 Nov 2024 20:27:46 +1100 Subject: [PATCH 12/20] Expand set of predefined machine-int mixed-type operations --- experiments/analytic-engine/src/eval.rs | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs index 0930d3fd..c8d0fdab 100644 --- a/experiments/analytic-engine/src/eval.rs +++ b/experiments/analytic-engine/src/eval.rs @@ -110,6 +110,20 @@ macro_rules! widening { }; } +macro_rules! mixed_widening { + ( $( $tr:ident, $meth:ident => $( ( $left_t:ty, $right_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + $( + pub fn $fname(lhs: $left_t, rhs: $right_t) -> Eval<$out_t> { + Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) + } + )* + )* + )* + }; +} + // SECTION - Combinatorial Explosion // SECTION - strictly homogenous (T -> T -> T) binary operations @@ -187,6 +201,67 @@ widening! { (i32 => (mul_i32_i64, i64)); } +mixed_widening! { + Add, add => + // <= Bits8 + (u8, i8 => (add_u8_i8_i16, i16), (add_u8_i8_u32, u32), (add_u8_i8_i32, i32), (add_u8_i8_i64, i64)), + (i8, u8 => (add_i8_u8_i16, i16), (add_i8_u8_u32, u32), (add_i8_u8_i32, i32), (add_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 + (u8, u16 => (add_u8_u16_u32, u32), (add_u8_u16_u64, u64), (add_u8_u16_i32, i32), (add_u8_u16_i64, i64)), + (u16, u8 => (add_u16_u8_u32, u32), (add_u16_u8_u64, u64), (add_u16_u8_i32, i32), (add_u16_u8_i64, i64)), + (i8, u16 => (add_i8_u16_i32, i32), (add_i8_u16_i64, i64)), + (u16, i8 => (add_u16_i8_i32, i32), (add_u16_i8_i64, i64)), + (u8, i16 => (add_u8_i16_i32, i32), (add_u8_i16_i64, i64)), + (i16, u8 => (add_i16_u8_i32, i32), (add_i16_u8_i64, i64)), + (i8, i16 => (add_i8_i16_i32, i32), (add_i8_i16_i64, i64)), + (i16, i8 => (add_i16_i8_i32, i32), (add_i16_i8_i64, i64)), + + (u16, i16 => (add_u16_i16_i32, i32), (add_u16_i16_i64, i64)), + (i16, u16 => (add_i16_u16_i32, i32), (add_i16_u16_i64, i64)), + + (u32, u8 => (add_u32_u8_u64, u64)), + (u8, u32 => (add_u8_u32_u64, u64)), + (u32, i8 => (add_u32_i8_u64, u64)), + (i8, u32 => (add_i8_u32_u64, u64)), + ; + Sub, sub => + // <= Bits8 + (u8, i8 => (sub_u8_i8_i16, i16), (sub_u8_i8_i32, i32), (sub_u8_i8_i64, i64)), + (i8, u8 => (sub_i8_u8_i16, i16), (sub_i8_u8_i32, i32), (sub_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 + (u8, u16 => (sub_u8_u16_i32, i32), (sub_u8_u16_i64, i64)), + (u16, u8 => (sub_u16_u8_i32, i32), (sub_u16_u8_i64, i64)), + (u8, i16 => (sub_u8_i16_i32, i32), (sub_u8_i16_i64, i64)), + (i16, u8 => (sub_i16_u8_i32, i32), (sub_i16_u8_i64, i64)), + (i8, u16 => (sub_i8_u16_i32, i32), (sub_i8_u16_i64, i64)), + (u16, i8 => (sub_u16_i8_i32, i32), (sub_u16_i8_i64, i64)), + (u16, i16 => (sub_u16_i16_i32, i32), (sub_u16_i16_i64, i64)), + (i16, u16 => (sub_i16_u16_i32, i32), (sub_i16_u16_i64, i64)), + (i8, i16 => (sub_i8_i16_i32, i32), (sub_i8_i16_i64, i64)), + (i16, i8 => (sub_i16_i8_i32, i32), (sub_i16_i8_i64, i64)), + + // >Bits16, <= Bits32 + (u32, u8 => (sub_u32_u8_i64, i64)), + (u8, u32 => (sub_u8_u32_i64, i64)), + (u32, i8 => (sub_u32_i8_i64, i64)), + (i8, u32 => (sub_i8_u32_i64, i64)), + (u32, u16 => (sub_u32_u16_i64, i64)), + (u16, u32 => (sub_u16_u32_i64, i64)), + (u32, i16 => (sub_u32_i16_i64, i64)), + (i16, u32 => (sub_i16_u32_i64, i64)), + + (i32, u8 => (sub_i32_u8_i64, i64)), + (u8, i32 => (sub_u8_i32_i64, i64)), + (i32, u16 => (sub_i32_u16_i64, i64)), + (u16, i32 => (sub_u16_i32_i64, i64)), + (i32, i8 => (sub_i32_i8_i64, i64)), + (i8, i32 => (sub_i8_i32_i64, i64)), + (i32, i16 => (sub_i32_i16_i64, i64)), + (i16, i32 => (sub_i16_i32_i64, i64)); +} + // !SECTION pub fn eval_fallback( From e4131b6004783b26d11d089ca6106b2a96ca5a71 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Thu, 7 Nov 2024 11:15:49 +1100 Subject: [PATCH 13/20] Retool op-definition macros, implement 480 common fns --- experiments/analytic-engine/src/eval.rs | 340 ++++++++++++++++++++---- 1 file changed, 287 insertions(+), 53 deletions(-) diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs index c8d0fdab..574ca6c0 100644 --- a/experiments/analytic-engine/src/eval.rs +++ b/experiments/analytic-engine/src/eval.rs @@ -102,7 +102,27 @@ macro_rules! widening { $( $( pub fn $fname(lhs: $in_t, rhs: $in_t) -> Eval<$out_t> { - Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) + match <$out_t as $tr>::$meth(&(lhs as $out_t), &(rhs as $out_t)) { + Some(res) => Eval::Direct(res), + None => Eval::Indirect(IndirectEval { value: Rc::new(LazyCell::new(Box::new(move || BigInt::$meth(&BigInt::from(lhs), &BigInt::from(rhs)).unwrap()))) }), + } + } + )* + )* + )* + }; +} + +macro_rules! widening_quotient { + ( $( $tr:ident, $meth:ident => $( ( $in_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + $( + pub fn $fname(lhs: $in_t, rhs: $in_t) -> Eval<$out_t> { + match <$out_t as $tr>::$meth(&(lhs as $out_t), &(rhs as $out_t)) { + Some(res) => Eval::Direct(res), + None => Eval::NaN, + } } )* )* @@ -110,13 +130,34 @@ macro_rules! widening { }; } +/// Macro for bulk definition of heterogenous source-type typed binary operations where the output type can represent every possible input type, but the computation might not succeed macro_rules! mixed_widening { ( $( $tr:ident, $meth:ident => $( ( $left_t:ty, $right_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { $( $( $( pub fn $fname(lhs: $left_t, rhs: $right_t) -> Eval<$out_t> { - Eval::Direct(<$out_t as $tr>::$meth((lhs as $out_t), (rhs as $out_t))) + match <$out_t as $tr>::$meth(&(lhs as $out_t), &(rhs as $out_t)) { + Some(res) => Eval::Direct(res), + None => Eval::Indirect(IndirectEval { value: Rc::new(LazyCell::new(Box::new(move || BigInt::$meth(&BigInt::from(lhs), &BigInt::from(rhs)).unwrap()))) }), + } + } + )* + )* + )* + }; +} + +macro_rules! mixed_widening_quotient { + ( $( $tr:ident, $meth:ident => $( ( $left_t:ty, $right_t:ty => $( ( $fname:ident, $out_t:ty ) ),+ $(,)? ) ),+ $(,)? );+ $(;)? ) => { + $( + $( + $( + pub fn $fname(lhs: $left_t, rhs: $right_t) -> Eval<$out_t> { + match <$out_t as $tr>::$meth(&(lhs as $out_t), &(rhs as $out_t)) { + Some(res) => Eval::Direct(res), + None => Eval::NaN, + } } )* )* @@ -126,7 +167,9 @@ macro_rules! mixed_widening { // SECTION - Combinatorial Explosion -// SECTION - strictly homogenous (T -> T -> T) binary operations +// SECTION - strictly homogenous (T -> T -> T) binary operations (40 total) + +// Responsible for 24 function definitions homogenous! { CheckedAdd, checked_add => (add_u8, u8), @@ -157,6 +200,8 @@ homogenous! { (mul_i64, i64); } +// (T, T) -> T binary operations involving quotients (failure is NaN) +// Responsible for 16 function definitions homogenous_quotient! { CheckedDiv, checked_div => (div_u8, u8), @@ -179,89 +224,278 @@ homogenous_quotient! { } // !SECTION +// SECTION - heterogenous-type binary operations ( `(T0, T0) -> T1` or `(T0, T1) -> T` ) +// (T0, T0) -> T1 binary operations where the values of T0 are a subset of T1 and failure indicates overflow/underflow + +// responsible for 42 function definitions widening! { - Add, add => + CheckedAdd, checked_add => (u8 => (add_u8_u16, u16), (add_u8_u32, u32), (add_u8_u64, u64), (add_u8_i16, i16), (add_u8_i32, i32), (add_u8_i64, i64)), (u16 => (add_u16_u32, u32), (add_u16_u64, u64), (add_u16_i32, i32), (add_u16_i64, i64)), (u32 => (add_u32_u64, u64), (add_u32_i64, i64)), - (i8 => (add_i8_i16, i16), (add_i8_i32, i32), (add_i8_i64, i64)); - Sub, sub => - (u8 => (sub_u8_i16, i16), (sub_u8_i32, i32), (sub_u8_i64, i64)), - (u16 => (sub_u16_i32, i32), (sub_u16_i64, i64)), - (u32 => (sub_u32_i64, i64)), + (i8 => (add_i8_i16, i16), (add_i8_i32, i32), (add_i8_i64, i64)), + (i16 => (add_i16_i32, i32), (add_i16_i64, i64)), + (i32 => (add_i32_i64, i64)); + CheckedSub, checked_sub => + (u8 => (sub_u8_u16, u16), (sub_u8_u32, u32), (sub_u8_u64, u64), (sub_u8_i16, i16), (sub_u8_i32, i32), (sub_u8_i64, i64)), + (u16 => (sub_u16_u32, u32), (sub_u16_u64, u64), (sub_u16_i32, i32), (sub_u16_i64, i64)), + (u32 => (sub_u32_u64, u64), (sub_u32_i64, i64)), (i8 => (sub_i8_i16, i16), (sub_i8_i32, i32), (sub_i8_i64, i64)), (i16 => (sub_i16_i32, i32), (sub_i16_i64, i64)), (i32 => (sub_i32_i64, i64)); - Mul, mul => - (u8 => (mul_u8_u16, u16), (mul_u8_u32, u32), (mul_u8_u64, u64), (mul_u8_i32, i32), (mul_u8_i64, i64)), - (u16 => (mul_u16_u32, u32), (mul_u16_u64, u64), (mul_u16_i64, i64)), - (u32 => (mul_u32_u64, u64)), + CheckedMul, checked_mul => + (u8 => (mul_u8_u16, u16), (mul_u8_u32, u32), (mul_u8_u64, u64), (mul_u8_i16, i16), (mul_u8_i32, i32), (mul_u8_i64, i64)), + (u16 => (mul_u16_u32, u32), (mul_u16_u64, u64), (mul_u16_i32, i32), (mul_u16_i64, i64)), + (u32 => (mul_u32_u64, u64), (mul_u32_i64, i64)), (i8 => (mul_i8_i16, i16), (mul_i8_i32, i32), (mul_i8_i64, i64)), (i16 => (mul_i16_i32, i32), (mul_i16_i64, i64)), (i32 => (mul_i32_i64, i64)); } +// (T0, T0) -> T1 binary operations where the values of T0 are a subset of T1 and failure indicates NaN +// Responsible for 28 function definitions +widening_quotient! { + CheckedDiv, checked_div => + // 14 functions + (u8 => (div_u8_u16, u16), (div_u8_u32, u32), (div_u8_u64, u64), (div_u8_i16, i16), (div_u8_i32, i32), (div_u8_i64, i64)), + (u16 => (div_u16_u32, u32), (div_u16_u64, u64), (div_u16_i32, i32), (div_u16_i64, i64)), + (u32 => (div_u32_u64, u64), (div_u32_i64, i64)), + (i8 => (div_i8_i16, i16), (div_i8_i32, i32), (div_i8_i64, i64)), + (i16 => (div_i16_i32, i32), (div_i16_i64, i64)), + (i32 => (div_i32_i64, i64)); + CheckedRem, checked_rem => + // 14 functions + (u8 => (rem_u8_u16, u16), (rem_u8_u32, u32), (rem_u8_u64, u64), (rem_u8_i16, i16), (rem_u8_i32, i32), (rem_u8_i64, i64)), // 6 + (u16 => (rem_u16_u32, u32), (rem_u16_u64, u64), (rem_u16_i32, i32), (rem_u16_i64, i64)), // 4 + (u32 => (rem_u32_u64, u64), (rem_u32_i64, i64)), // 2 + (i8 => (rem_i8_i16, i16), (rem_i8_i32, i32), (rem_i8_i64, i64)), // 3 + (i16 => (rem_i16_i32, i32), (rem_i16_i64, i64)), // 2 + (i32 => (rem_i32_i64, i64)); // 1 +} + +// (T0, T1) -> T binary operations +// Responsible for 210 functions mixed_widening! { - Add, add => - // <= Bits8 - (u8, i8 => (add_u8_i8_i16, i16), (add_u8_i8_u32, u32), (add_u8_i8_i32, i32), (add_u8_i8_i64, i64)), - (i8, u8 => (add_i8_u8_i16, i16), (add_i8_u8_u32, u32), (add_i8_u8_i32, i32), (add_i8_u8_i64, i64)), - - // >Bits8, <= Bits16 - (u8, u16 => (add_u8_u16_u32, u32), (add_u8_u16_u64, u64), (add_u8_u16_i32, i32), (add_u8_u16_i64, i64)), - (u16, u8 => (add_u16_u8_u32, u32), (add_u16_u8_u64, u64), (add_u16_u8_i32, i32), (add_u16_u8_i64, i64)), - (i8, u16 => (add_i8_u16_i32, i32), (add_i8_u16_i64, i64)), + CheckedAdd, checked_add => + // <= Bits8 (6 functions) + (u8, i8 => (add_u8_i8_i16, i16), (add_u8_i8_i32, i32), (add_u8_i8_i64, i64)), + (i8, u8 => (add_i8_u8_i16, i16), (add_i8_u8_i32, i32), (add_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 (30 functions) + (u16, u8 => (add_u16_u8_u16, u16), (add_u16_u8_u32, u32), (add_u16_u8_u64, u64), (add_u16_u8_i32, i32), (add_u16_u8_i64, i64)), + (u8, u16 => (add_u8_u16_u16, u16), (add_u8_u16_u32, u32), (add_u8_u16_u64, u64), (add_u8_u16_i32, i32), (add_u8_u16_i64, i64)), (u16, i8 => (add_u16_i8_i32, i32), (add_u16_i8_i64, i64)), - (u8, i16 => (add_u8_i16_i32, i32), (add_u8_i16_i64, i64)), - (i16, u8 => (add_i16_u8_i32, i32), (add_i16_u8_i64, i64)), - (i8, i16 => (add_i8_i16_i32, i32), (add_i8_i16_i64, i64)), - (i16, i8 => (add_i16_i8_i32, i32), (add_i16_i8_i64, i64)), + (i8, u16 => (add_i8_u16_i32, i32), (add_i8_u16_i64, i64)), + + (i16, u8 => (add_i16_u8_i16, i16), (add_i16_u8_i32, i32), (add_i16_u8_i64, i64)), + (u8, i16 => (add_u8_i16_i16, i16), (add_u8_i16_i32, i32), (add_u8_i16_i64, i64)), + (i16, i8 => (add_i16_i8_i16, i16), (add_i16_i8_i32, i32), (add_i16_i8_i64, i64)), + (i8, i16 => (add_i8_i16_i16, i16), (add_i8_i16_i32, i32), (add_i8_i16_i64, i64)), (u16, i16 => (add_u16_i16_i32, i32), (add_u16_i16_i64, i64)), (i16, u16 => (add_i16_u16_i32, i32), (add_i16_u16_i64, i64)), - (u32, u8 => (add_u32_u8_u64, u64)), - (u8, u32 => (add_u8_u32_u64, u64)), - (u32, i8 => (add_u32_i8_u64, u64)), - (i8, u32 => (add_i8_u32_u64, u64)), + // >Bits16, <=Bits32 (34 functions) + (u32, u8 => (add_u32_u8_u32, u32), (add_u32_u8_u64, u64), (add_u32_u8_i64, i64)), + (u8, u32 => (add_u8_u32_u32, u32), (add_u8_u32_u64, u64), (add_u8_u32_i64, i64)), + (u32, i8 => (add_u32_i8_i64, i64)), + (i8, u32 => (add_i8_u32_i64, i64)), + + (u32, u16 => (add_u32_u16_u32, u32), (add_u32_u16_u64, u64), (add_u32_u16_i64, i64)), + (u16, u32 => (add_u16_u32_u32, u32), (add_u16_u32_u64, u64), (add_u16_u32_i64, i64)), + (u32, i16 => (add_u32_i16_i64, i64)), + (i16, u32 => (add_i16_u32_i64, i64)), + + (i32, u8 => (add_i32_u8_i32, i32), (add_i32_u8_i64, i64)), + (u8, i32 => (add_u8_i32_i32, i32), (add_u8_i32_i64, i64)), + (i32, i8 => (add_i32_i8_i32, i32), (add_i32_i8_i64, i64)), + (i8, i32 => (add_i8_i32_i32, i32), (add_i8_i32_i64, i64)), + + (i32, u16 => (add_i32_u16_i32, i32), (add_i32_u16_i64, i64)), + (u16, i32 => (add_u16_i32_i32, i32), (add_u16_i32_i64, i64)), + (i32, i16 => (add_i32_i16_i32, i32), (add_i32_i16_i64, i64)), + (i16, i32 => (add_i16_i32_i32, i32), (add_i16_i32_i64, i64)), + + (u32, i32 => (add_u32_i32_i64, i64)), + (i32, u32 => (add_i32_u32_i64, i64)), ; - Sub, sub => - // <= Bits8 + CheckedSub, checked_sub => + // <= Bits8 (6 functions) (u8, i8 => (sub_u8_i8_i16, i16), (sub_u8_i8_i32, i32), (sub_u8_i8_i64, i64)), (i8, u8 => (sub_i8_u8_i16, i16), (sub_i8_u8_i32, i32), (sub_i8_u8_i64, i64)), - // >Bits8, <= Bits16 - (u8, u16 => (sub_u8_u16_i32, i32), (sub_u8_u16_i64, i64)), - (u16, u8 => (sub_u16_u8_i32, i32), (sub_u16_u8_i64, i64)), - (u8, i16 => (sub_u8_i16_i32, i32), (sub_u8_i16_i64, i64)), - (i16, u8 => (sub_i16_u8_i32, i32), (sub_i16_u8_i64, i64)), - (i8, u16 => (sub_i8_u16_i32, i32), (sub_i8_u16_i64, i64)), + // >Bits8, <= Bits16 (30 functions) + (u16, u8 => (sub_u16_u8_u16, u16), (sub_u16_u8_u32, u32), (sub_u16_u8_u64, u64), (sub_u16_u8_i32, i32), (sub_u16_u8_i64, i64)), + (u8, u16 => (sub_u8_u16_u16, u16), (sub_u8_u16_u32, u32), (sub_u8_u16_u64, u64), (sub_u8_u16_i32, i32), (sub_u8_u16_i64, i64)), (u16, i8 => (sub_u16_i8_i32, i32), (sub_u16_i8_i64, i64)), + (i8, u16 => (sub_i8_u16_i32, i32), (sub_i8_u16_i64, i64)), + + (u8, i16 => (sub_u8_i16_i16, i16), (sub_u8_i16_i32, i32), (sub_u8_i16_i64, i64)), + (i16, u8 => (sub_i16_u8_i16, i16), (sub_i16_u8_i32, i32), (sub_i16_u8_i64, i64)), + (i8, i16 => (sub_i8_i16_i16, i16), (sub_i8_i16_i32, i32), (sub_i8_i16_i64, i64)), + (i16, i8 => (sub_i16_i8_i16, i16), (sub_i16_i8_i32, i32), (sub_i16_i8_i64, i64)), + (u16, i16 => (sub_u16_i16_i32, i32), (sub_u16_i16_i64, i64)), (i16, u16 => (sub_i16_u16_i32, i32), (sub_i16_u16_i64, i64)), - (i8, i16 => (sub_i8_i16_i32, i32), (sub_i8_i16_i64, i64)), - (i16, i8 => (sub_i16_i8_i32, i32), (sub_i16_i8_i64, i64)), - // >Bits16, <= Bits32 - (u32, u8 => (sub_u32_u8_i64, i64)), - (u8, u32 => (sub_u8_u32_i64, i64)), + // >Bits16, <= Bits32 (34 functions) + (u32, u8 => (sub_u32_u8_u32, u32), (sub_u32_u8_u64, u64), (sub_u32_u8_i64, i64)), + (u8, u32 => (sub_u8_u32_u32, u32), (sub_u8_u32_u64, u64), (sub_u8_u32_i64, i64)), (u32, i8 => (sub_u32_i8_i64, i64)), (i8, u32 => (sub_i8_u32_i64, i64)), - (u32, u16 => (sub_u32_u16_i64, i64)), - (u16, u32 => (sub_u16_u32_i64, i64)), + + (u32, u16 => (sub_u32_u16_u32, u32), (sub_u32_u16_u64, u64), (sub_u32_u16_i64, i64)), + (u16, u32 => (sub_u16_u32_u32, u32), (sub_u16_u32_u64, u64), (sub_u16_u32_i64, i64)), (u32, i16 => (sub_u32_i16_i64, i64)), (i16, u32 => (sub_i16_u32_i64, i64)), - (i32, u8 => (sub_i32_u8_i64, i64)), - (u8, i32 => (sub_u8_i32_i64, i64)), - (i32, u16 => (sub_i32_u16_i64, i64)), - (u16, i32 => (sub_u16_i32_i64, i64)), - (i32, i8 => (sub_i32_i8_i64, i64)), - (i8, i32 => (sub_i8_i32_i64, i64)), - (i32, i16 => (sub_i32_i16_i64, i64)), - (i16, i32 => (sub_i16_i32_i64, i64)); + (i32, u8 => (sub_i32_u8_i32, i32), (sub_i32_u8_i64, i64)), + (u8, i32 => (sub_u8_i32_i32, i32), (sub_u8_i32_i64, i64)), + (i32, i8 => (sub_i32_i8_i32, i32), (sub_i32_i8_i64, i64)), + (i8, i32 => (sub_i8_i32_i32, i32), (sub_i8_i32_i64, i64)), + + (i32, u16 => (sub_i32_u16_i32, i32), (sub_i32_u16_i64, i64)), + (u16, i32 => (sub_u16_i32_i32, i32), (sub_u16_i32_i64, i64)), + (i32, i16 => (sub_i32_i16_i32, i32), (sub_i32_i16_i64, i64)), + (i16, i32 => (sub_i16_i32_i32, i32), (sub_i16_i32_i64, i64)), + + (u32, i32 => (sub_u32_i32_i64, i64)), + (i32, u32 => (sub_i32_u32_i64, i64)); + CheckedMul, checked_mul => + // <= Bits8 (6 functions) + (u8, i8 => (mul_u8_i8_i16, i16), (mul_u8_i8_i32, i32), (mul_u8_i8_i64, i64)), + (i8, u8 => (mul_i8_u8_i16, i16), (mul_i8_u8_i32, i32), (mul_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 (30 functions) + (u16, u8 => (mul_u16_u8_u16, u16), (mul_u16_u8_u32, u32), (mul_u16_u8_u64, u64), (mul_u16_u8_i32, i32), (mul_u16_u8_i64, i64)), + (u8, u16 => (mul_u8_u16_u16, u16), (mul_u8_u16_u32, u32), (mul_u8_u16_u64, u64), (mul_u8_u16_i32, i32), (mul_u8_u16_i64, i64)), + (u16, i8 => (mul_u16_i8_i32, i32), (mul_u16_i8_i64, i64)), + (i8, u16 => (mul_i8_u16_i32, i32), (mul_i8_u16_i64, i64)), + + (u8, i16 => (mul_u8_i16_i16, i16), (mul_u8_i16_i32, i32), (mul_u8_i16_i64, i64)), + (i16, u8 => (mul_i16_u8_i16, i16), (mul_i16_u8_i32, i32), (mul_i16_u8_i64, i64)), + (i8, i16 => (mul_i8_i16_i16, i16), (mul_i8_i16_i32, i32), (mul_i8_i16_i64, i64)), + (i16, i8 => (mul_i16_i8_i16, i16), (mul_i16_i8_i32, i32), (mul_i16_i8_i64, i64)), + + (u16, i16 => (mul_u16_i16_i32, i32), (mul_u16_i16_i64, i64)), + (i16, u16 => (mul_i16_u16_i32, i32), (mul_i16_u16_i64, i64)), + + // >Bits16, <= Bits32 (34 functions) + (u32, u8 => (mul_u32_u8_u32, u32), (mul_u32_u8_u64, u64), (mul_u32_u8_i64, i64)), + (u8, u32 => (mul_u8_u32_u32, u32), (mul_u8_u32_u64, u64), (mul_u8_u32_i64, i64)), + (u32, i8 => (mul_u32_i8_i64, i64)), + (i8, u32 => (mul_i8_u32_i64, i64)), + + (u32, u16 => (mul_u32_u16_u32, u32), (mul_u32_u16_u64, u64), (mul_u32_u16_i64, i64)), + (u16, u32 => (mul_u16_u32_u32, u32), (mul_u16_u32_u64, u64), (mul_u16_u32_i64, i64)), + (u32, i16 => (mul_u32_i16_i64, i64)), + (i16, u32 => (mul_i16_u32_i64, i64)), + + (i32, u8 => (mul_i32_u8_i32, i32), (mul_i32_u8_i64, i64)), + (u8, i32 => (mul_u8_i32_i32, i32), (mul_u8_i32_i64, i64)), + (i32, i8 => (mul_i32_i8_i32, i32), (mul_i32_i8_i64, i64)), + (i8, i32 => (mul_i8_i32_i32, i32), (mul_i8_i32_i64, i64)), + + (i32, u16 => (mul_i32_u16_i32, i32), (mul_i32_u16_i64, i64)), + (u16, i32 => (mul_u16_i32_i32, i32), (mul_u16_i32_i64, i64)), + (i32, i16 => (mul_i32_i16_i32, i32), (mul_i32_i16_i64, i64)), + (i16, i32 => (mul_i16_i32_i32, i32), (mul_i16_i32_i64, i64)), + + (u32, i32 => (mul_u32_i32_i64, i64)), + (i32, u32 => (mul_i32_u32_i64, i64)); } +// Responsible for 140 functions +mixed_widening_quotient! { + CheckedDiv, checked_div => + // <= Bits8 (6 functions) + (u8, i8 => (div_u8_i8_i16, i16), (div_u8_i8_i32, i32), (div_u8_i8_i64, i64)), + (i8, u8 => (div_i8_u8_i16, i16), (div_i8_u8_i32, i32), (div_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 (30 functions) + (u16, u8 => (div_u16_u8_u16, u16), (div_u16_u8_u32, u32), (div_u16_u8_u64, u64), (div_u16_u8_i32, i32), (div_u16_u8_i64, i64)), + (u8, u16 => (div_u8_u16_u16, u16), (div_u8_u16_u32, u32), (div_u8_u16_u64, u64), (div_u8_u16_i32, i32), (div_u8_u16_i64, i64)), + (u16, i8 => (div_u16_i8_i32, i32), (div_u16_i8_i64, i64)), + (i8, u16 => (div_i8_u16_i32, i32), (div_i8_u16_i64, i64)), + + (i16, u8 => (div_i16_u8_i16, i16), (div_i16_u8_i32, i32), (div_i16_u8_i64, i64)), + (u8, i16 => (div_u8_i16_i16, i16), (div_u8_i16_i32, i32), (div_u8_i16_i64, i64)), + (i16, i8 => (div_i16_i8_i16, i16), (div_i16_i8_i32, i32), (div_i16_i8_i64, i64)), + (i8, i16 => (div_i8_i16_i16, i16), (div_i8_i16_i32, i32), (div_i8_i16_i64, i64)), + + (u16, i16 => (div_u16_i16_i32, i32), (div_u16_i16_i64, i64)), + (i16, u16 => (div_i16_u16_i32, i32), (div_i16_u16_i64, i64)), + + // >Bits16, <=Bits32 (34 functions) + (u32, u8 => (div_u32_u8_u32, u32), (div_u32_u8_u64, u64), (div_u32_u8_i64, i64)), + (u8, u32 => (div_u8_u32_u32, u32), (div_u8_u32_u64, u64), (div_u8_u32_i64, i64)), + (u32, i8 => (div_u32_i8_i64, i64)), + (i8, u32 => (div_i8_u32_i64, i64)), + + (u32, u16 => (div_u32_u16_u32, u32), (div_u32_u16_u64, u64), (div_u32_u16_i64, i64)), + (u16, u32 => (div_u16_u32_u32, u32), (div_u16_u32_u64, u64), (div_u16_u32_i64, i64)), + (u32, i16 => (div_u32_i16_i64, i64)), + (i16, u32 => (div_i16_u32_i64, i64)), + + (i32, u8 => (div_i32_u8_i32, i32), (div_i32_u8_i64, i64)), + (u8, i32 => (div_u8_i32_i32, i32), (div_u8_i32_i64, i64)), + (i32, i8 => (div_i32_i8_i32, i32), (div_i32_i8_i64, i64)), + (i8, i32 => (div_i8_i32_i32, i32), (div_i8_i32_i64, i64)), + + (i32, u16 => (div_i32_u16_i32, i32), (div_i32_u16_i64, i64)), + (u16, i32 => (div_u16_i32_i32, i32), (div_u16_i32_i64, i64)), + (i32, i16 => (div_i32_i16_i32, i32), (div_i32_i16_i64, i64)), + (i16, i32 => (div_i16_i32_i32, i32), (div_i16_i32_i64, i64)), + + + (u32, i32 => (div_u32_i32_i64, i64)), + (i32, u32 => (div_i32_u32_i64, i64)), + ; + CheckedRem, checked_rem => + // <= Bits8 (6 functions) + (u8, i8 => (rem_u8_i8_i16, i16), (rem_u8_i8_i32, i32), (rem_u8_i8_i64, i64)), + (i8, u8 => (rem_i8_u8_i16, i16), (rem_i8_u8_i32, i32), (rem_i8_u8_i64, i64)), + + // >Bits8, <= Bits16 (30 functions) + (u16, u8 => (rem_u16_u8_u16, u16), (rem_u16_u8_u32, u32), (rem_u16_u8_u64, u64), (rem_u16_u8_i32, i32), (rem_u16_u8_i64, i64)), + (u8, u16 => (rem_u8_u16_u16, u16), (rem_u8_u16_u32, u32), (rem_u8_u16_u64, u64), (rem_u8_u16_i32, i32), (rem_u8_u16_i64, i64)), + (u16, i8 => (rem_u16_i8_i32, i32), (rem_u16_i8_i64, i64)), + (i8, u16 => (rem_i8_u16_i32, i32), (rem_i8_u16_i64, i64)), + + (u8, i16 => (rem_u8_i16_i16, i16), (rem_u8_i16_i32, i32), (rem_u8_i16_i64, i64)), + (i16, u8 => (rem_i16_u8_i16, i16), (rem_i16_u8_i32, i32), (rem_i16_u8_i64, i64)), + (i8, i16 => (rem_i8_i16_i16, i16), (rem_i8_i16_i32, i32), (rem_i8_i16_i64, i64)), + (i16, i8 => (rem_i16_i8_i16, i16), (rem_i16_i8_i32, i32), (rem_i16_i8_i64, i64)), + + (u16, i16 => (rem_u16_i16_i32, i32), (rem_u16_i16_i64, i64)), + (i16, u16 => (rem_i16_u16_i32, i32), (rem_i16_u16_i64, i64)), + + // >Bits16, <= Bits32 (34 functions) + (u32, u8 => (rem_u32_u8_u32, u32), (rem_u32_u8_u64, u64), (rem_u32_u8_i64, i64)), + (u8, u32 => (rem_u8_u32_u32, u32), (rem_u8_u32_u64, u64), (rem_u8_u32_i64, i64)), + (u32, i8 => (rem_u32_i8_i64, i64)), + (i8, u32 => (rem_i8_u32_i64, i64)), + + (u32, u16 => (rem_u32_u16_u32, u32), (rem_u32_u16_u64, u64), (rem_u32_u16_i64, i64)), + (u16, u32 => (rem_u16_u32_u32, u32), (rem_u16_u32_u64, u64), (rem_u16_u32_i64, i64)), + (u32, i16 => (rem_u32_i16_i64, i64)), + (i16, u32 => (rem_i16_u32_i64, i64)), + + (i32, u8 => (rem_i32_u8_i32, i32), (rem_i32_u8_i64, i64)), + (u8, i32 => (rem_u8_i32_i32, i32), (rem_u8_i32_i64, i64)), + (i32, i8 => (rem_i32_i8_i32, i32), (rem_i32_i8_i64, i64)), + (i8, i32 => (rem_i8_i32_i32, i32), (rem_i8_i32_i64, i64)), + + (i32, u16 => (rem_i32_u16_i32, i32), (rem_i32_u16_i64, i64)), + (u16, i32 => (rem_u16_i32_i32, i32), (rem_u16_i32_i64, i64)), + (i32, i16 => (rem_i32_i16_i32, i32), (rem_i32_i16_i64, i64)), + (i16, i32 => (rem_i16_i32_i32, i32), (rem_i16_i32_i64, i64)), + + (u32, i32 => (rem_u32_i32_i64, i64)), + (i32, u32 => (rem_i32_u32_i64, i64)); +} +// !SECTION + // !SECTION pub fn eval_fallback( From 3914e63c673229c82eb0649d1df6b06b611971b5 Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Thu, 7 Nov 2024 11:24:41 +1100 Subject: [PATCH 14/20] Remove unused imports of std::ops from eval --- experiments/analytic-engine/src/eval.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs index 574ca6c0..f580fe81 100644 --- a/experiments/analytic-engine/src/eval.rs +++ b/experiments/analytic-engine/src/eval.rs @@ -2,7 +2,6 @@ use num_bigint::BigInt; use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub}; use std::any::type_name; use std::cell::LazyCell; -use std::ops::{Add, Mul, Sub}; use std::rc::Rc; #[derive(Clone)] From 3613edc4595a28c0fd39c0459c4bc386c086f29d Mon Sep 17 00:00:00 2001 From: Peter Duchovni Date: Thu, 7 Nov 2024 14:58:29 +1100 Subject: [PATCH 15/20] Prototype code-generation simulacrum --- experiments/analytic-engine/src/core.rs | 13 + experiments/analytic-engine/src/elaborator.rs | 56 ++- experiments/analytic-engine/src/eval.rs | 77 +++- experiments/analytic-engine/src/gen.rs | 410 ++++++++++++++++++ experiments/analytic-engine/src/lib.rs | 1 + experiments/analytic-engine/src/printer.rs | 48 +- 6 files changed, 581 insertions(+), 24 deletions(-) create mode 100644 experiments/analytic-engine/src/gen.rs diff --git a/experiments/analytic-engine/src/core.rs b/experiments/analytic-engine/src/core.rs index b5ebdbab..a648b9fa 100644 --- a/experiments/analytic-engine/src/core.rs +++ b/experiments/analytic-engine/src/core.rs @@ -183,6 +183,19 @@ impl NumRep { pub const fn is_auto(self) -> bool { matches!(self, NumRep::Auto) } + + /// Returns true if `self` and `other` are both concrete types, and the bounds of `self` + /// entirely encompass the bounds of `other` (i.e. every value within the assignable range of `other` is representable within self, including when the two are equal). + pub(crate) fn encompasses(&self, other: &Self) -> bool { + let Some(self_bounds) = self.as_bounds() else { + return false; + }; + let Some(other_bounds) = other.as_bounds() else { + return false; + }; + + self_bounds.encompasses(&other_bounds) + } } #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)] diff --git a/experiments/analytic-engine/src/elaborator.rs b/experiments/analytic-engine/src/elaborator.rs index 4dfd1424..211851b0 100644 --- a/experiments/analytic-engine/src/elaborator.rs +++ b/experiments/analytic-engine/src/elaborator.rs @@ -12,6 +12,28 @@ pub(crate) enum PrimInt { I64, } +impl PrimInt { + pub const fn to_static_str(self) -> &'static str { + match self { + PrimInt::U8 => "u8", + PrimInt::U16 => "u16", + PrimInt::U32 => "u32", + PrimInt::U64 => "u64", + PrimInt::I8 => "i8", + PrimInt::I16 => "i16", + PrimInt::I32 => "i32", + PrimInt::I64 => "i64", + } + } + + pub(crate) fn is_unsigned(&self) -> bool { + matches!( + self, + PrimInt::U8 | PrimInt::U16 | PrimInt::U32 | PrimInt::U64 + ) + } +} + pub(crate) const PRIM_INTS: [PrimInt; 8] = [ PrimInt::U8, PrimInt::U16, @@ -81,16 +103,14 @@ pub(crate) enum IntType { } impl IntType { + pub fn to_prim(self) -> PrimInt { + let IntType::Prim(ret) = self; + ret + } + pub const fn to_static_str(self) -> &'static str { match self { - IntType::Prim(PrimInt::U8) => "u8", - IntType::Prim(PrimInt::U16) => "u16", - IntType::Prim(PrimInt::U32) => "u32", - IntType::Prim(PrimInt::U64) => "u64", - IntType::Prim(PrimInt::I8) => "i8", - IntType::Prim(PrimInt::I16) => "i16", - IntType::Prim(PrimInt::I32) => "i32", - IntType::Prim(PrimInt::I64) => "i64", + Self::Prim(p) => p.to_static_str(), } } } @@ -111,7 +131,7 @@ pub(crate) enum TypedExpr { Box>, ), ElabUnaryOp(TypeRep, TypedUnaryOp, Box>), - ElabCast(TypeRep, NumRep, Box>), + ElabCast(TypeRep, TypedCast, Box>), } impl TypedExpr { @@ -125,8 +145,8 @@ impl TypedExpr { } } -type Sig1 = (T, T); -type Sig2 = ((T, T), T); +pub(crate) type Sig1 = (T, T); +pub(crate) type Sig2 = ((T, T), T); #[derive(Clone, Debug)] pub(crate) struct TypedBinOp { @@ -140,6 +160,11 @@ pub(crate) struct TypedUnaryOp { pub(crate) inner: UnaryOp, } +#[derive(Clone, Copy, Debug)] +pub(crate) struct TypedCast { + pub(crate) sig: Sig1, + pub(crate) _rep: NumRep, +} pub(crate) mod inference { use std::collections::HashSet; @@ -1118,7 +1143,14 @@ impl Elaborator { Expr::Cast(rep, inner) => { let t_inner = self.elaborate_expr(inner)?; let t = self.get_type_from_index(index)?; - Ok(TypedExpr::ElabCast(t, *rep, Box::new(t_inner))) + Ok(TypedExpr::ElabCast( + t, + TypedCast { + sig: (*t_inner.get_type(), t), + _rep: *rep, + }, + Box::new(t_inner), + )) } } } diff --git a/experiments/analytic-engine/src/eval.rs b/experiments/analytic-engine/src/eval.rs index f580fe81..0f35f05b 100644 --- a/experiments/analytic-engine/src/eval.rs +++ b/experiments/analytic-engine/src/eval.rs @@ -1,4 +1,4 @@ -use num_bigint::BigInt; +use num_bigint::{BigInt, TryFromBigIntError}; use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedRem, CheckedSub}; use std::any::type_name; use std::cell::LazyCell; @@ -49,6 +49,23 @@ where } } +pub enum EvalError { + Indirect(BigInt), + NotANumber, +} + +impl Eval { + pub fn eval(&self) -> Result { + match self { + Eval::NaN => Err(EvalError::NotANumber), + Eval::Direct(x) => Ok(*x), + Eval::Indirect(IndirectEval { value, .. }) => { + Err(EvalError::Indirect((&***value).clone())) + } + } + } +} + // !SECTION /// Macro for bulk definition of homogenously typed binary operations with a checked version provided by a trait (`num_traits`), where @@ -505,7 +522,7 @@ pub fn eval_fallback( ) -> Eval where BigInt: From + From, - Res: TryFrom, + Res: TryFrom>, { eprintln!("[INFO]: encountered fallback operation `{_op_hint} : (({}, {}) -> {})` that may benefit from standalone function", type_name::(), type_name::(), type_name::() @@ -514,15 +531,57 @@ where let big_r = BigInt::from(rhs); let o_big_res = checked_op(&big_l, &big_r); if let Some(big_res) = o_big_res { - let _big_res = big_res.clone(); - if let Ok(res) = >::try_from(big_res) { - Eval::Direct(res) - } else { - Eval::Indirect(IndirectEval { - value: Rc::new(LazyCell::new(Box::new(move || _big_res))), - }) + match >::try_from(big_res) { + Ok(res) => Eval::Direct(res), + Err(e) => Eval::Indirect(IndirectEval { + value: Rc::new(LazyCell::new(Box::new(move || e.into_original()))), + }), } } else { Eval::NaN } } + +// FIXME - to save time we are not defining any first-class unary operations but we should +pub fn eval_unary_fallback( + x: In, + _op_hint: &'static str, + op: impl FnOnce(&BigInt) -> BigInt, +) -> Eval +where + BigInt: From, + Out: TryFrom>, +{ + eprintln!("[INFO]: encountered fallback operation `{_op_hint} : ({} -> {})` that may benefit from standalone function", type_name::(), type_name::()); + let big_x = BigInt::from(x); + let big_res = op(&big_x); + match >::try_from(big_res) { + Ok(res) => Eval::Direct(res), + Err(e) => Eval::Indirect(IndirectEval { + value: Rc::new(LazyCell::new(Box::new(move || e.into_original()))), + }), + } +} + +pub fn cast_fallback(x: In) -> Eval +where + BigInt: From, + Out: TryFrom>, +{ + eprintln!("[INFO]: encountered fallback cast of type `({} -> {})` that may benefit from standalone function", type_name::(), type_name::()); + let big_x = BigInt::from(x); + match >::try_from(big_x) { + Ok(y) => Eval::Direct(y), + Err(e) => Eval::Indirect(IndirectEval { + value: Rc::new(LazyCell::new(Box::new(move || e.into_original()))), + }), + } +} + +#[inline] +/// Noop function that 'performs' absolute value computation on unsigned integers (where input and output types are the same) +pub const fn abs_noop(x: T) -> T { + x +} + +// TODO - add in 'as'-cast abs for unsigned->wider unsigned conversions, as well as other unary cases diff --git a/experiments/analytic-engine/src/gen.rs b/experiments/analytic-engine/src/gen.rs new file mode 100644 index 00000000..6d201f23 --- /dev/null +++ b/experiments/analytic-engine/src/gen.rs @@ -0,0 +1,410 @@ +use std::borrow::Cow; + +use crate::core::{BasicBinOp, BasicUnaryOp, BinOp, NumRep, TypedConst, UnaryOp}; +use crate::elaborator::{IntType, PrimInt, Sig1, Sig2, TypedBinOp, TypedExpr, TypedUnaryOp}; +use crate::printer::{ + fragment::Fragment, + precedence::{cond_paren, Precedence}, +}; + +pub(crate) mod ast { + use crate::{core::TypedConst, elaborator::PrimInt}; + + pub type Label = std::borrow::Cow<'static, str>; + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum SType { + RustPrimInt(PrimInt), + } + + #[derive(Clone, Debug)] + pub enum AstEntity { + Unqualified(Label), + Qualified(Vec