From 04d62f3c92000624f214f9ce4dd03c8b01618250 Mon Sep 17 00:00:00 2001 From: Ryan Helminen Date: Sun, 13 Jul 2025 12:56:38 -0400 Subject: [PATCH 01/10] Implement bitwise operators. --- examples/parse_error/unexpected_character.nbt | 1 - .../runtime_error/nonscalar_bitwise_not.nbt | 1 + examples/tests/bitwise.nbt | 30 ++++++++ numbat/src/ast.rs | 11 +++ numbat/src/bytecode_interpreter.rs | 9 +++ numbat/src/diagnostic.rs | 3 +- numbat/src/interpreter/mod.rs | 2 + numbat/src/number.rs | 48 +++++++++++++ numbat/src/parser.rs | 56 +++++++++++++++ numbat/src/quantity.rs | 48 +++++++++++++ numbat/src/tokenizer.rs | 50 +++++++++---- numbat/src/typechecker/const_evaluation.rs | 16 +++++ numbat/src/typechecker/error.rs | 3 + numbat/src/typechecker/mod.rs | 21 ++++++ numbat/src/typed_ast.rs | 3 + numbat/src/vm.rs | 72 +++++++++++++++++++ 16 files changed, 359 insertions(+), 15 deletions(-) delete mode 100644 examples/parse_error/unexpected_character.nbt create mode 100644 examples/runtime_error/nonscalar_bitwise_not.nbt create mode 100644 examples/tests/bitwise.nbt diff --git a/examples/parse_error/unexpected_character.nbt b/examples/parse_error/unexpected_character.nbt deleted file mode 100644 index f7d55c7d9..000000000 --- a/examples/parse_error/unexpected_character.nbt +++ /dev/null @@ -1 +0,0 @@ -2 & 3 diff --git a/examples/runtime_error/nonscalar_bitwise_not.nbt b/examples/runtime_error/nonscalar_bitwise_not.nbt new file mode 100644 index 000000000..e2dfd8095 --- /dev/null +++ b/examples/runtime_error/nonscalar_bitwise_not.nbt @@ -0,0 +1 @@ +~2.2 \ No newline at end of file diff --git a/examples/tests/bitwise.nbt b/examples/tests/bitwise.nbt new file mode 100644 index 000000000..68a4c85ff --- /dev/null +++ b/examples/tests/bitwise.nbt @@ -0,0 +1,30 @@ +# bitwise and truth table +assert_eq(1 & 1, 1) +assert_eq(1 & 0, 0) +assert_eq(0 & 1, 0) +assert_eq(0 & 0, 0) + +# bitwise or truth table +assert_eq(1 | 1, 1) +assert_eq(1 | 0, 1) +assert_eq(0 | 1, 1) +assert_eq(0 | 0, 0) + +# bitwise xor truth table +assert_eq(0 ⨁ 0, 0) +assert_eq(1 ⨁ 0, 1) +assert_eq(0 ⨁ 1, 1) +assert_eq(1 ⨁ 1, 0) + +# bitshift left checks +assert_eq(0xFF << 2, 0x3FC) +assert_eq(0xFF << 0, 0xFF) + +# bitshift right checks +assert_eq(0xC7A5 >> 8, 0xC7) + +# bit flipping operation +assert_eq(0xC700 ⨁ (0xC0 << 8), 0x700) + +# bit clearing operation +assert_eq(0xC700 & ~(1 << 15), 0x4700) \ No newline at end of file diff --git a/numbat/src/ast.rs b/numbat/src/ast.rs index ae0ed2df6..b4af1280f 100644 --- a/numbat/src/ast.rs +++ b/numbat/src/ast.rs @@ -16,6 +16,7 @@ pub enum UnaryOperator { Factorial(NonZeroUsize), Negate, LogicalNeg, + BitwiseNot, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -34,6 +35,11 @@ pub enum BinaryOperator { NotEqual, LogicalAnd, LogicalOr, + BitwiseOr, + BitwiseAnd, + BitwiseXor, + BitShiftLeft, + BitShiftRight, } impl PrettyPrint for BinaryOperator { @@ -55,6 +61,11 @@ impl PrettyPrint for BinaryOperator { NotEqual => "≠", LogicalAnd => "&&", LogicalOr => "||", + BitwiseOr => "|", + BitwiseAnd => "&", + BitwiseXor => "⨁", + BitShiftLeft => "<<", + BitShiftRight => ">>", }); match self { diff --git a/numbat/src/bytecode_interpreter.rs b/numbat/src/bytecode_interpreter.rs index b2f7f0d73..412955c28 100644 --- a/numbat/src/bytecode_interpreter.rs +++ b/numbat/src/bytecode_interpreter.rs @@ -109,6 +109,10 @@ impl BytecodeInterpreter { self.compile_expression(lhs)?; self.vm.add_op(Op::LogicalNeg); } + Expression::UnaryOperator(_span, UnaryOperator::BitwiseNot, lhs, _type) => { + self.compile_expression(lhs)?; + self.vm.add_op(Op::BitwiseNot); + } Expression::BinaryOperator(_span, operator, lhs, rhs, _type) => { self.compile_expression(lhs)?; self.compile_expression(rhs)?; @@ -128,6 +132,11 @@ impl BytecodeInterpreter { BinaryOperator::NotEqual => Op::NotEqual, BinaryOperator::LogicalAnd => Op::LogicalAnd, BinaryOperator::LogicalOr => Op::LogicalOr, + BinaryOperator::BitwiseOr => Op::BitwiseOr, + BinaryOperator::BitwiseAnd => Op::BitwiseAnd, + BinaryOperator::BitwiseXor => Op::BitwiseXor, + BinaryOperator::BitShiftLeft => Op::BitShiftLeft, + BinaryOperator::BitShiftRight => Op::BitShiftRight, }; self.vm.add_op(op); } diff --git a/numbat/src/diagnostic.rs b/numbat/src/diagnostic.rs index ca6750d5d..848c20866 100644 --- a/numbat/src/diagnostic.rs +++ b/numbat/src/diagnostic.rs @@ -125,7 +125,8 @@ impl ErrorDiagnostic for TypeCheckError { d.with_labels(labels).with_notes(vec![inner_error]) } TypeCheckError::NonScalarExponent(span, type_) - | TypeCheckError::NonScalarFactorialArgument(span, type_) => d + | TypeCheckError::NonScalarFactorialArgument(span, type_) + | TypeCheckError::NonScalarBitwiseNotArgument(span, type_) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(format!("{type_}"))]) diff --git a/numbat/src/interpreter/mod.rs b/numbat/src/interpreter/mod.rs index bf8dc7f0f..00d980e1e 100644 --- a/numbat/src/interpreter/mod.rs +++ b/numbat/src/interpreter/mod.rs @@ -27,6 +27,8 @@ pub enum RuntimeError { FactorialOfNegativeNumber, #[error("Expected factorial argument to be a finite integer number")] FactorialOfNonInteger, + #[error("Expected bitwise not argument to be a finite integer number")] + BitwiseNotOfNonInteger, #[error("{0}")] UnitRegistryError(UnitRegistryError), // TODO: can this even be triggered? #[error("{0}")] diff --git a/numbat/src/number.rs b/numbat/src/number.rs index 47286396f..884eed837 100644 --- a/numbat/src/number.rs +++ b/numbat/src/number.rs @@ -136,6 +136,46 @@ impl std::ops::Mul for Number { } } +impl std::ops::BitOr for Number { + type Output = Number; + + fn bitor(self, rhs: Self) -> Self::Output { + Number((self.0 as i64 | rhs.0 as i64) as f64) + } +} + +impl std::ops::BitAnd for Number { + type Output = Number; + + fn bitand(self, rhs: Self) -> Self::Output { + Number((self.0 as i64 & rhs.0 as i64) as f64) + } +} + +impl std::ops::BitXor for Number { + type Output = Number; + + fn bitxor(self, rhs: Self) -> Self::Output { + Number((self.0 as i64 ^ rhs.0 as i64) as f64) + } +} + +impl std::ops::Shl for Number { + type Output = Number; + + fn shl(self, rhs: Self) -> Self::Output { + Number(((self.0 as i64) << rhs.0 as i64) as f64) + } +} + +impl std::ops::Shr for Number { + type Output = Number; + + fn shr(self, rhs: Self) -> Self::Output { + Number(((self.0 as i64) >> (rhs.0 as i64)) as f64) + } +} + impl std::ops::Div for Number { type Output = Number; @@ -152,6 +192,14 @@ impl std::ops::Neg for Number { } } +impl std::ops::Not for Number { + type Output = Number; + + fn not(self) -> Self::Output { + Number(!(self.0 as i64) as f64) + } +} + impl std::iter::Product for Number { fn product>(iter: I) -> Self { iter.fold(Number::from_f64(1.0), |acc, n| acc * n) diff --git a/numbat/src/parser.rs b/numbat/src/parser.rs index 6f4343e2e..7fce34d83 100644 --- a/numbat/src/parser.rs +++ b/numbat/src/parser.rs @@ -1139,6 +1139,15 @@ impl<'a> Parser<'a> { tokens, &[TokenKind::LogicalOr], |_| BinaryOperator::LogicalOr, + |parser| parser.bitwise_or(tokens), + ) + } + + fn bitwise_or(&mut self, tokens: &[Token<'a>]) -> Result> { + self.parse_binop( + tokens, + &[TokenKind::BitwiseOr], + |_| BinaryOperator::BitwiseOr, |parser| parser.logical_and(tokens), ) } @@ -1148,6 +1157,42 @@ impl<'a> Parser<'a> { tokens, &[TokenKind::LogicalAnd], |_| BinaryOperator::LogicalAnd, + |parser| parser.bitwise_and(tokens), + ) + } + + fn bitwise_and(&mut self, tokens: &[Token<'a>]) -> Result> { + self.parse_binop( + tokens, + &[TokenKind::BitwiseAnd], + |_| BinaryOperator::BitwiseAnd, + |parser| parser.bitwise_xor(tokens), + ) + } + + fn bitwise_xor(&mut self, tokens: &[Token<'a>]) -> Result> { + self.parse_binop( + tokens, + &[TokenKind::BitwiseXor], + |_| BinaryOperator::BitwiseXor, + |parser| parser.bitshift_left(tokens), + ) + } + + fn bitshift_left(&mut self, tokens: &[Token<'a>]) -> Result> { + self.parse_binop( + tokens, + &[TokenKind::BitShiftLeft], + |_| BinaryOperator::BitShiftLeft, + |parser| parser.bitshift_right(tokens), + ) + } + + fn bitshift_right(&mut self, tokens: &[Token<'a>]) -> Result> { + self.parse_binop( + tokens, + &[TokenKind::BitShiftRight], + |_| BinaryOperator::BitShiftRight, |parser| parser.logical_neg(tokens), ) } @@ -1239,6 +1284,15 @@ impl<'a> Parser<'a> { expr: Box::new(rhs), span_op: span, }) + } else if self.match_exact(tokens, TokenKind::BitwiseNot).is_some() { + let span = self.last(tokens).unwrap().span; + let rhs = self.unary(tokens)?; + + Ok(Expression::UnaryOperator { + op: UnaryOperator::BitwiseNot, + expr: Box::new(rhs), + span_op: span, + }) } else if self.match_exact(tokens, TokenKind::Plus).is_some() { // A unary `+` is equivalent to nothing. We can get rid of the // symbol without inserting any nodes in the AST. @@ -3189,6 +3243,8 @@ mod tests { "###); } + // TODO: Add test for bitwise operations + #[test] fn logical_operation() { // basic diff --git a/numbat/src/quantity.rs b/numbat/src/quantity.rs index 9664b3712..c26504868 100644 --- a/numbat/src/quantity.rs +++ b/numbat/src/quantity.rs @@ -306,6 +306,46 @@ impl std::ops::Mul for Quantity { } } +impl std::ops::BitOr for Quantity { + type Output = Quantity; + + fn bitor(self, rhs: Self) -> Self::Output { + Quantity::new(self.value | rhs.value, self.unit.clone()) + } +} + +impl std::ops::BitAnd for Quantity { + type Output = Quantity; + + fn bitand(self, rhs: Self) -> Self::Output { + Quantity::new(self.value & rhs.value, self.unit.clone()) + } +} + +impl std::ops::BitXor for Quantity { + type Output = Quantity; + + fn bitxor(self, rhs: Self) -> Self::Output { + Quantity::new(self.value ^ rhs.value, self.unit.clone()) + } +} + +impl std::ops::Shl for Quantity { + type Output = Quantity; + + fn shl(self, rhs: Self) -> Self::Output { + Quantity::new(self.value << rhs.value, self.unit.clone()) + } +} + +impl std::ops::Shr for Quantity { + type Output = Quantity; + + fn shr(self, rhs: Self) -> Self::Output { + Quantity::new(self.value >> rhs.value, self.unit.clone()) + } +} + impl std::ops::Div for Quantity { type Output = Quantity; @@ -314,6 +354,14 @@ impl std::ops::Div for Quantity { } } +impl std::ops::Not for Quantity { + type Output = Quantity; + + fn not(self) -> Self::Output { + Quantity::new(!self.value, self.unit) + } +} + impl std::ops::Neg for Quantity { type Output = Quantity; diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index bf149ec3d..bac6251c0 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -87,6 +87,12 @@ pub enum TokenKind { GreaterOrEqual, LogicalAnd, LogicalOr, + BitwiseOr, + BitwiseAnd, + BitwiseNot, + BitwiseXor, + BitShiftLeft, + BitShiftRight, Period, QuestionMark, @@ -480,9 +486,11 @@ impl Tokenizer { '}' if !self.interpolation_state.is_inside() => TokenKind::RightCurly, '≤' => TokenKind::LessOrEqual, '<' if self.match_char(input, '=') => TokenKind::LessOrEqual, + '<' if self.match_char(input, '<') => TokenKind::BitShiftLeft, '<' => TokenKind::LessThan, '≥' => TokenKind::GreaterOrEqual, '>' if self.match_char(input, '=') => TokenKind::GreaterOrEqual, + '>' if self.match_char(input, '>') => TokenKind::BitShiftRight, '>' => TokenKind::GreaterThan, '?' => TokenKind::QuestionMark, '0' if self @@ -572,6 +580,10 @@ impl Tokenizer { '|' if self.match_char(input, '|') => TokenKind::LogicalOr, '|' if self.match_char(input, '>') => TokenKind::PostfixApply, '*' if self.match_char(input, '*') => TokenKind::Power, + '|' => TokenKind::BitwiseOr, + '&' => TokenKind::BitwiseAnd, + '⨁' => TokenKind::BitwiseXor, + '~' => TokenKind::BitwiseNot, '+' => TokenKind::Plus, '*' | '·' | '⋅' | '×' => TokenKind::Multiply, '/' => TokenKind::Divide, @@ -900,9 +912,31 @@ fn test_tokenize_basic() { [("...", Ellipsis, ByteIndex(0)), ("", Eof, ByteIndex(3))] ); - insta::assert_snapshot!( - tokenize_reduced_pretty("~").unwrap_err(), - @"Error at index 0: `Unexpected character: '~'`"); + assert_eq!( + tokenize_reduced("1<<2\n42").unwrap(), + [ + ("1", Number, ByteIndex(0)), + ("<<", BitShiftLeft, ByteIndex(1)), + ("2", Number, ByteIndex(3)), + ("\n", Newline, ByteIndex(4)), + ("42", Number, ByteIndex(5)), + ("", Eof, ByteIndex(7)) + ] + ); + + assert_eq!( + tokenize_reduced("1|2&42").unwrap(), + [ + ("1", Number, ByteIndex(0)), + ("|", BitwiseOr, ByteIndex(1)), + ("2", Number, ByteIndex(2)), + ("&", BitwiseAnd, ByteIndex(3)), + ("42", Number, ByteIndex(4)), + ("", Eof, ByteIndex(6)) + ] + ); + + } #[test] @@ -1194,16 +1228,6 @@ fn test_logical_operators() { "", Eof, 13 "### ); - - insta::assert_snapshot!( - tokenize_reduced_pretty("true | false").unwrap_err(), - @"Error at index 5: `Unexpected character: '|'`" - ); - - insta::assert_snapshot!( - tokenize_reduced_pretty("true & false").unwrap_err(), - @"Error at index 5: `Unexpected character: '&'`" - ); } #[test] diff --git a/numbat/src/typechecker/const_evaluation.rs b/numbat/src/typechecker/const_evaluation.rs index b3cd4aa69..5ba0bce0c 100644 --- a/numbat/src/typechecker/const_evaluation.rs +++ b/numbat/src/typechecker/const_evaluation.rs @@ -25,6 +25,7 @@ pub fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { "factorial" } typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::LogicalNeg, _, _) => "logical", + typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::BitwiseNot, _, _) => "bitwise", e @ typed_ast::Expression::BinaryOperator(_span_op, op, lhs_expr, rhs_expr, _) => { let lhs = evaluate_const_expr(lhs_expr)?; @@ -83,6 +84,21 @@ pub fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { "logical", ))) } + typed_ast::BinaryOperator::BitwiseOr + | typed_ast::BinaryOperator::BitwiseAnd + | typed_ast::BinaryOperator::BitwiseXor => { + Err(Box::new(TypeCheckError::UnsupportedConstEvalExpression( + e.full_span(), + "bitwise", + ))) + } + typed_ast::BinaryOperator::BitShiftLeft + | typed_ast::BinaryOperator::BitShiftRight => { + Err(Box::new(TypeCheckError::UnsupportedConstEvalExpression( + e.full_span(), + "bitshift", + ))) + } }; } typed_ast::Expression::Identifier(..) => "variable", diff --git a/numbat/src/typechecker/error.rs b/numbat/src/typechecker/error.rs index dd416db3c..b5578cf71 100644 --- a/numbat/src/typechecker/error.rs +++ b/numbat/src/typechecker/error.rs @@ -24,6 +24,9 @@ pub enum TypeCheckError { #[error("Argument of factorial needs to be dimensionless (got {1}).")] NonScalarFactorialArgument(Span, Type), + #[error("Argument of bitwise not needs to be dimensionless (got {1}).")] + NonScalarBitwiseNotArgument(Span, Type), + #[error("Unsupported expression in const-evaluation of exponent: {1}.")] UnsupportedConstEvalExpression(Span, &'static str), diff --git a/numbat/src/typechecker/mod.rs b/numbat/src/typechecker/mod.rs index 0217642d4..d27f8559b 100644 --- a/numbat/src/typechecker/mod.rs +++ b/numbat/src/typechecker/mod.rs @@ -368,6 +368,17 @@ impl TypeChecker { ))); } } + ast::UnaryOperator::BitwiseNot => { + if self + .add_equal_constraint(&type_, &Type::scalar()) + .is_trivially_violated() + { + return Err(Box::new(TypeCheckError::NonScalarBitwiseNotArgument( + expr.full_span(), + type_, + ))); + } + } ast::UnaryOperator::Negate => { self.enforce_dtype(&type_, expr.full_span())?; } @@ -523,6 +534,11 @@ impl TypeChecker { } typed_ast::BinaryOperator::LogicalAnd => "and".into(), typed_ast::BinaryOperator::LogicalOr => "or".into(), + typed_ast::BinaryOperator::BitwiseOr => "bitwise or".into(), + typed_ast::BinaryOperator::BitwiseAnd => "bitwise and".into(), + typed_ast::BinaryOperator::BitwiseXor => "bitwise xor".into(), + typed_ast::BinaryOperator::BitShiftLeft => "bit shift left".into(), + typed_ast::BinaryOperator::BitShiftRight => "bit shift right".into(), }, span_expected: lhs.full_span(), expected_name: " left hand side", @@ -550,6 +566,11 @@ impl TypeChecker { let type_ = match op { typed_ast::BinaryOperator::Add => get_type_and_assert_equal_dtypes()?, typed_ast::BinaryOperator::Sub => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitwiseOr => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitwiseAnd => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitwiseXor => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitShiftLeft => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitShiftRight => get_type_and_assert_equal_dtypes()?, typed_ast::BinaryOperator::Mul | typed_ast::BinaryOperator::Div => { let type_lhs = lhs_checked.get_type(); let type_rhs = rhs_checked.get_type(); diff --git a/numbat/src/typed_ast.rs b/numbat/src/typed_ast.rs index 16decf70a..0c15b8839 100644 --- a/numbat/src/typed_ast.rs +++ b/numbat/src/typed_ast.rs @@ -1300,6 +1300,9 @@ impl PrettyPrint for Expression<'_> { UnaryOperator(_, self::UnaryOperator::LogicalNeg, expr, _type) => { m::operator("!") + with_parens(expr) } + UnaryOperator(_, self::UnaryOperator::BitwiseNot, expr, _type) => { + m::operator("~") + with_parens(expr) + } BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), BinaryOperatorForDate(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), FunctionCall(_, _, name, args, _type) => { diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index dd3ec68cd..30e57ee32 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -77,6 +77,12 @@ pub enum Op { NotEqual, LogicalAnd, LogicalOr, + BitwiseOr, + BitwiseAnd, + BitwiseXor, + BitwiseNot, + BitShiftLeft, + BitShiftRight, LogicalNeg, /// Similar to Add, but has DateTime on the LHS and a quantity on the RHS @@ -138,6 +144,7 @@ impl Op { | Op::AccessStructField | Op::BuildList => 1, Op::Negate + | Op::BitwiseNot | Op::Factorial | Op::Add | Op::AddToDateTime @@ -156,6 +163,11 @@ impl Op { | Op::NotEqual | Op::LogicalAnd | Op::LogicalOr + | Op::BitwiseOr + | Op::BitwiseAnd + | Op::BitwiseXor + | Op::BitShiftRight + | Op::BitShiftLeft | Op::LogicalNeg | Op::Return | Op::GetLastResult => 0, @@ -189,6 +201,12 @@ impl Op { Op::NotEqual => "NotEqual", Op::LogicalAnd => "LogicalAnd", Op::LogicalOr => "LogicalOr", + Op::BitwiseOr => "BitwiseOr", + Op::BitwiseNot => "BitwiseNot", + Op::BitwiseAnd => "BitwiseAnd", + Op::BitwiseXor => "BitwiseXor", + Op::BitShiftLeft => "BitShiftLeft", + Op::BitShiftRight => "BitShiftRight", Op::LogicalNeg => "LogicalNeg", Op::JumpIfFalse => "JumpIfFalse", Op::Jump => "Jump", @@ -799,10 +817,64 @@ impl Vm { }; self.push_bool(result); } + Op::BitwiseOr => { + let rhs = self.pop_quantity(); + let lhs = self.pop_quantity(); + + let result = Ok(lhs | rhs); + + self.push_quantity(result.map_err(RuntimeError::QuantityError)?); + } + Op::BitwiseAnd => { + let rhs = self.pop_quantity(); + let lhs = self.pop_quantity(); + + let result = Ok(lhs & rhs); + + self.push_quantity(result.map_err(RuntimeError::QuantityError)?); + } + Op::BitwiseXor => { + let rhs = self.pop_quantity(); + let lhs = self.pop_quantity(); + + let result = Ok(lhs ^ rhs); + + self.push_quantity(result.map_err(RuntimeError::QuantityError)?); + } + Op::BitShiftLeft => { + let rhs = self.pop_quantity(); + let lhs = self.pop_quantity(); + + let result = Ok(lhs << rhs); + + self.push_quantity(result.map_err(RuntimeError::QuantityError)?); + } + Op::BitShiftRight => { + let rhs = self.pop_quantity(); + let lhs = self.pop_quantity(); + + let result = Ok(lhs >> rhs); + + self.push_quantity(result.map_err(RuntimeError::QuantityError)?); + } Op::LogicalNeg => { let rhs = self.pop_bool(); self.push_bool(!rhs); } + Op::BitwiseNot => { + let rhs = self.pop_quantity(); + + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise not operand to be scalar") + .to_f64(); + + + if check_rhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseNotOfNonInteger)); + } + self.push_quantity(!rhs); + } Op::Negate => { let rhs = self.pop_quantity(); self.push_quantity(-rhs); From 09aec75d022dc933bca55058f10373e63312e614 Mon Sep 17 00:00:00 2001 From: Ryan Helminen Date: Sun, 13 Jul 2025 13:18:23 -0400 Subject: [PATCH 02/10] Cargo fmt formatting updates. --- numbat/src/diagnostic.rs | 2 +- numbat/src/tokenizer.rs | 2 -- numbat/src/typechecker/const_evaluation.rs | 22 ++++++--------- numbat/src/typechecker/mod.rs | 32 ++++++++++++++++------ numbat/src/vm.rs | 7 ++--- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/numbat/src/diagnostic.rs b/numbat/src/diagnostic.rs index 848c20866..da247f581 100644 --- a/numbat/src/diagnostic.rs +++ b/numbat/src/diagnostic.rs @@ -125,7 +125,7 @@ impl ErrorDiagnostic for TypeCheckError { d.with_labels(labels).with_notes(vec![inner_error]) } TypeCheckError::NonScalarExponent(span, type_) - | TypeCheckError::NonScalarFactorialArgument(span, type_) + | TypeCheckError::NonScalarFactorialArgument(span, type_) | TypeCheckError::NonScalarBitwiseNotArgument(span, type_) => d .with_labels(vec![span .diagnostic_label(LabelStyle::Primary) diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index bac6251c0..0de237ec5 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -935,8 +935,6 @@ fn test_tokenize_basic() { ("", Eof, ByteIndex(6)) ] ); - - } #[test] diff --git a/numbat/src/typechecker/const_evaluation.rs b/numbat/src/typechecker/const_evaluation.rs index 5ba0bce0c..636dde5f7 100644 --- a/numbat/src/typechecker/const_evaluation.rs +++ b/numbat/src/typechecker/const_evaluation.rs @@ -84,21 +84,15 @@ pub fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { "logical", ))) } - typed_ast::BinaryOperator::BitwiseOr - | typed_ast::BinaryOperator::BitwiseAnd - | typed_ast::BinaryOperator::BitwiseXor => { - Err(Box::new(TypeCheckError::UnsupportedConstEvalExpression( - e.full_span(), - "bitwise", - ))) - } + typed_ast::BinaryOperator::BitwiseOr + | typed_ast::BinaryOperator::BitwiseAnd + | typed_ast::BinaryOperator::BitwiseXor => Err(Box::new( + TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "bitwise"), + )), typed_ast::BinaryOperator::BitShiftLeft - | typed_ast::BinaryOperator::BitShiftRight => { - Err(Box::new(TypeCheckError::UnsupportedConstEvalExpression( - e.full_span(), - "bitshift", - ))) - } + | typed_ast::BinaryOperator::BitShiftRight => Err(Box::new( + TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "bitshift"), + )), }; } typed_ast::Expression::Identifier(..) => "variable", diff --git a/numbat/src/typechecker/mod.rs b/numbat/src/typechecker/mod.rs index d27f8559b..14983d302 100644 --- a/numbat/src/typechecker/mod.rs +++ b/numbat/src/typechecker/mod.rs @@ -535,10 +535,18 @@ impl TypeChecker { typed_ast::BinaryOperator::LogicalAnd => "and".into(), typed_ast::BinaryOperator::LogicalOr => "or".into(), typed_ast::BinaryOperator::BitwiseOr => "bitwise or".into(), - typed_ast::BinaryOperator::BitwiseAnd => "bitwise and".into(), - typed_ast::BinaryOperator::BitwiseXor => "bitwise xor".into(), - typed_ast::BinaryOperator::BitShiftLeft => "bit shift left".into(), - typed_ast::BinaryOperator::BitShiftRight => "bit shift right".into(), + typed_ast::BinaryOperator::BitwiseAnd => { + "bitwise and".into() + } + typed_ast::BinaryOperator::BitwiseXor => { + "bitwise xor".into() + } + typed_ast::BinaryOperator::BitShiftLeft => { + "bit shift left".into() + } + typed_ast::BinaryOperator::BitShiftRight => { + "bit shift right".into() + } }, span_expected: lhs.full_span(), expected_name: " left hand side", @@ -567,10 +575,18 @@ impl TypeChecker { typed_ast::BinaryOperator::Add => get_type_and_assert_equal_dtypes()?, typed_ast::BinaryOperator::Sub => get_type_and_assert_equal_dtypes()?, typed_ast::BinaryOperator::BitwiseOr => get_type_and_assert_equal_dtypes()?, - typed_ast::BinaryOperator::BitwiseAnd => get_type_and_assert_equal_dtypes()?, - typed_ast::BinaryOperator::BitwiseXor => get_type_and_assert_equal_dtypes()?, - typed_ast::BinaryOperator::BitShiftLeft => get_type_and_assert_equal_dtypes()?, - typed_ast::BinaryOperator::BitShiftRight => get_type_and_assert_equal_dtypes()?, + typed_ast::BinaryOperator::BitwiseAnd => { + get_type_and_assert_equal_dtypes()? + } + typed_ast::BinaryOperator::BitwiseXor => { + get_type_and_assert_equal_dtypes()? + } + typed_ast::BinaryOperator::BitShiftLeft => { + get_type_and_assert_equal_dtypes()? + } + typed_ast::BinaryOperator::BitShiftRight => { + get_type_and_assert_equal_dtypes()? + } typed_ast::BinaryOperator::Mul | typed_ast::BinaryOperator::Div => { let type_lhs = lhs_checked.get_type(); let type_rhs = rhs_checked.get_type(); diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index 30e57ee32..c917b4ac9 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -865,10 +865,9 @@ impl Vm { let rhs = self.pop_quantity(); let check_rhs = rhs - .as_scalar() - .expect("Expected bitwise not operand to be scalar") - .to_f64(); - + .as_scalar() + .expect("Expected bitwise not operand to be scalar") + .to_f64(); if check_rhs.fract() != 0. { return Err(Box::new(RuntimeError::BitwiseNotOfNonInteger)); From 038c142ff5e08dc4cb0f469e65d0b5ca9e7e943f Mon Sep 17 00:00:00 2001 From: Ryan Helminen Date: Tue, 15 Jul 2025 21:02:06 -0400 Subject: [PATCH 03/10] =?UTF-8?q?Add=20'xor'=20as=20alternative=20to=20?= =?UTF-8?q?=E2=A8=81=20for=20performing=20xor=20operation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- numbat/src/tokenizer.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index 0de237ec5..c8b8f3cbb 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -582,6 +582,12 @@ impl Tokenizer { '*' if self.match_char(input, '*') => TokenKind::Power, '|' => TokenKind::BitwiseOr, '&' => TokenKind::BitwiseAnd, + 'x' if self.peek(input) == Some('o') && self.peek2(input) == Some('r') => { + self.advance(input); + self.advance(input); + + TokenKind::BitwiseXor + } '⨁' => TokenKind::BitwiseXor, '~' => TokenKind::BitwiseNot, '+' => TokenKind::Plus, From 61ea3d272f38444e42351c5bf679d914eb8b1b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20M=C3=B8ller=20Jensen?= Date: Wed, 16 Jul 2025 22:36:38 +0200 Subject: [PATCH 04/10] Fix compile error --- numbat/src/bytecode_interpreter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numbat/src/bytecode_interpreter.rs b/numbat/src/bytecode_interpreter.rs index a1e56ffaf..3c781c884 100644 --- a/numbat/src/bytecode_interpreter.rs +++ b/numbat/src/bytecode_interpreter.rs @@ -110,7 +110,7 @@ impl BytecodeInterpreter { self.vm.add_op(Op::LogicalNeg); } Expression::UnaryOperator(_span, UnaryOperator::BitwiseNot, lhs, _type) => { - self.compile_expression(lhs)?; + self.compile_expression(lhs); self.vm.add_op(Op::BitwiseNot); } Expression::BinaryOperator(_span, operator, lhs, rhs, _type) => { From bfaae3195aff364fa44f938043446ab30fad9412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20M=C3=B8ller=20Jensen?= Date: Wed, 16 Jul 2025 22:49:25 +0200 Subject: [PATCH 05/10] Add 'xor' keyword --- examples/tests/bitwise.nbt | 6 ++++++ numbat/src/keywords.rs | 1 + numbat/src/tokenizer.rs | 7 +------ vscode-extension/syntaxes/numbat.tmLanguage.json | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/tests/bitwise.nbt b/examples/tests/bitwise.nbt index 68a4c85ff..8ce1c00b4 100644 --- a/examples/tests/bitwise.nbt +++ b/examples/tests/bitwise.nbt @@ -16,6 +16,11 @@ assert_eq(1 ⨁ 0, 1) assert_eq(0 ⨁ 1, 1) assert_eq(1 ⨁ 1, 0) +assert_eq(0 xor 0, 0 ⨁ 0) +assert_eq(1 xor 0, 1 ⨁ 0) +assert_eq(0 xor 1, 0 ⨁ 1) +assert_eq(1 xor 1, 1 ⨁ 1) + # bitshift left checks assert_eq(0xFF << 2, 0x3FC) assert_eq(0xFF << 0, 0xFF) @@ -25,6 +30,7 @@ assert_eq(0xC7A5 >> 8, 0xC7) # bit flipping operation assert_eq(0xC700 ⨁ (0xC0 << 8), 0x700) +assert_eq(0xC700 xor (0xC0 << 8), 0x700) # bit clearing operation assert_eq(0xC700 & ~(1 << 15), 0x4700) \ No newline at end of file diff --git a/numbat/src/keywords.rs b/numbat/src/keywords.rs index 62c69a572..068441c4b 100644 --- a/numbat/src/keywords.rs +++ b/numbat/src/keywords.rs @@ -11,6 +11,7 @@ pub const KEYWORDS: &[&str] = &[ "unit ", "use ", "struct ", + "xor ", // 'inline' keywords "long", "short", diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index 432598971..d414628fb 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -501,6 +501,7 @@ impl Tokenizer { m.insert("false", TokenKind::False); m.insert("NaN", TokenKind::NaN); m.insert("inf", TokenKind::Inf); + m.insert("xor", TokenKind::BitwiseXor); // procedures m.insert(ProcedureKind::Print.name(), TokenKind::ProcedurePrint); @@ -653,12 +654,6 @@ impl Tokenizer { '*' if self.match_char(input, '*') => TokenKind::Power, '|' => TokenKind::BitwiseOr, '&' => TokenKind::BitwiseAnd, - 'x' if self.peek(input) == Some('o') && self.peek2(input) == Some('r') => { - self.advance(input); - self.advance(input); - - TokenKind::BitwiseXor - } '⨁' => TokenKind::BitwiseXor, '~' => TokenKind::BitwiseNot, '+' => TokenKind::Plus, diff --git a/vscode-extension/syntaxes/numbat.tmLanguage.json b/vscode-extension/syntaxes/numbat.tmLanguage.json index 6eacb21e1..6ec29a68c 100644 --- a/vscode-extension/syntaxes/numbat.tmLanguage.json +++ b/vscode-extension/syntaxes/numbat.tmLanguage.json @@ -32,7 +32,7 @@ "patterns": [ { "name": "keyword.control.numbat", - "match": "\\b(per|to|let|fn|where|and|dimension|unit|use|struct|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type)\\b" + "match": "\\b(per|to|let|fn|where|and|dimension|unit|use|struct|long|short|both|none|if|then|else|true|false|print|assert|assert_eq|type|xor)\\b" } ] }, From b46f890e550868a10a346de7a9fbacd83e059ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20M=C3=B8ller=20Jensen?= Date: Thu, 17 Jul 2025 00:14:34 +0200 Subject: [PATCH 06/10] Add variant of 'xor' unicode character --- numbat/src/tokenizer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index d414628fb..7d36daad3 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -654,7 +654,7 @@ impl Tokenizer { '*' if self.match_char(input, '*') => TokenKind::Power, '|' => TokenKind::BitwiseOr, '&' => TokenKind::BitwiseAnd, - '⨁' => TokenKind::BitwiseXor, + '⨁' | '⊕' => TokenKind::BitwiseXor, '~' => TokenKind::BitwiseNot, '+' => TokenKind::Plus, '*' | '·' | '⋅' | '×' => TokenKind::Multiply, From 9c18567924f0b6d9f52992cf14f83f41d25648f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20M=C3=B8ller=20Jensen?= Date: Thu, 17 Jul 2025 00:22:09 +0200 Subject: [PATCH 07/10] Add bitwise operators to documentation --- book/src/example-numbat_syntax.md | 3 +++ book/src/operations.md | 7 +++++++ examples/numbat_syntax.nbt | 3 +++ 3 files changed, 13 insertions(+) diff --git a/book/src/example-numbat_syntax.md b/book/src/example-numbat_syntax.md index 44145178a..6c3331045 100644 --- a/book/src/example-numbat_syntax.md +++ b/book/src/example-numbat_syntax.md @@ -52,6 +52,9 @@ mod(17, 4) # Modulo 3 in -> cm # Unit conversion, can also be → or ➞ 3 in to cm # Unit conversion with the 'to' keyword +0b011 ⊕ 0b110 # bitwise xor operator +0b011 xor 0b110 # bitwise xor with the 'xor' keyword + cos(pi/3 + pi) # Call mathematical functions pi/3 + pi |> cos # Same, 'arg |> f' is equivalent to 'f(arg)' # The '|>' operator has the lowest precedence diff --git a/book/src/operations.md b/book/src/operations.md index 3c8b89ba9..b21a1d368 100644 --- a/book/src/operations.md +++ b/book/src/operations.md @@ -9,6 +9,7 @@ Numbat operators and other language constructs, ordered by precedence form *high | exponentiation | `x^y`, `x**y` | | multiplication (implicit) | `x y` (*whitespace*) | | unary negation | `-x` | +| bitwise 'not' | `~x` | | division | `x per y` | | division | `x / y`, `x ÷ y` | | multiplication (explicit) | `x * y`, `x · y`, `x × y` | @@ -16,7 +17,12 @@ Numbat operators and other language constructs, ordered by precedence form *high | addition | `x + y` | | comparisons | `x < y`, `x <= y`, `x ≤ y`, … `x == y`, `x != y` | | logical negation | `!x` | +| bitwise shift right | `x >> y` | +| bitwise shift left | `x << y` | +| bitwise 'xor' | `x ⨁ y`, `x xor y` | +| bitwise 'and' | `x & y` | | logical 'and' | `x && y` | +| bitwise 'or' | x | y | | logical 'or' | x || y | | unit conversion | `x -> y`, `x → y`, `x ➞ y`, `x to y` | | conditionals | `if x then y else z` | @@ -28,6 +34,7 @@ Also, note that `per`-division has a higher precedence than `/`-division. This m If in doubt, you can always look at the pretty-printing output (second line in the snippet below) to make sure that your input was parsed correctly: + ``` numbat >>> 1 / meter per second diff --git a/examples/numbat_syntax.nbt b/examples/numbat_syntax.nbt index 73f55a4f8..cdca34ff6 100644 --- a/examples/numbat_syntax.nbt +++ b/examples/numbat_syntax.nbt @@ -47,6 +47,9 @@ mod(17, 4) # Modulo 3 in -> cm # Unit conversion, can also be → or ➞ 3 in to cm # Unit conversion with the 'to' keyword +0b011 ⊕ 0b110 # bitwise xor operator +0b011 xor 0b110 # bitwise xor with the 'xor' keyword + cos(pi/3 + pi) # Call mathematical functions pi/3 + pi |> cos # Same, 'arg |> f' is equivalent to 'f(arg)' # The '|>' operator has the lowest precedence From 95d817177d6edfb45596dddf20b9823f8f6bf086 Mon Sep 17 00:00:00 2001 From: Ryan Helminen Date: Sat, 19 Jul 2025 10:56:09 -0400 Subject: [PATCH 08/10] Check that operands of bitwise operations are integer quantities. Performing a bitwise operation on a float with a fractional value is undefined behavior. If it looks like an integer, it is safe to type cast. If the number value has a fractional component, it is likely better to bail out with a runtime error rather than silently truncating the fractional part. --- numbat/src/interpreter/mod.rs | 4 +- numbat/src/vm.rs | 72 ++++++++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/numbat/src/interpreter/mod.rs b/numbat/src/interpreter/mod.rs index 00d980e1e..0388e6c7e 100644 --- a/numbat/src/interpreter/mod.rs +++ b/numbat/src/interpreter/mod.rs @@ -27,8 +27,8 @@ pub enum RuntimeError { FactorialOfNegativeNumber, #[error("Expected factorial argument to be a finite integer number")] FactorialOfNonInteger, - #[error("Expected bitwise not argument to be a finite integer number")] - BitwiseNotOfNonInteger, + #[error("Expected bitwise argument(s) to be a finite integer number")] + BitwiseOperationOfNonInteger, #[error("{0}")] UnitRegistryError(UnitRegistryError), // TODO: can this even be triggered? #[error("{0}")] diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index c917b4ac9..857ffdcd9 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -821,6 +821,20 @@ impl Vm { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + let check_lhs = lhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + if check_rhs.fract() != 0. || check_lhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } + let result = Ok(lhs | rhs); self.push_quantity(result.map_err(RuntimeError::QuantityError)?); @@ -829,6 +843,20 @@ impl Vm { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + let check_lhs = lhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + if check_rhs.fract() != 0. || check_lhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } + let result = Ok(lhs & rhs); self.push_quantity(result.map_err(RuntimeError::QuantityError)?); @@ -837,6 +865,20 @@ impl Vm { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise not operand to be scalar") + .to_f64(); + + let check_lhs = lhs + .as_scalar() + .expect("Expected bitwise not operand to be scalar") + .to_f64(); + + if check_rhs.fract() != 0. || check_lhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } + let result = Ok(lhs ^ rhs); self.push_quantity(result.map_err(RuntimeError::QuantityError)?); @@ -845,6 +887,20 @@ impl Vm { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + let check_lhs = lhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + if check_rhs.fract() != 0. || check_lhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } + let result = Ok(lhs << rhs); self.push_quantity(result.map_err(RuntimeError::QuantityError)?); @@ -853,6 +909,20 @@ impl Vm { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); + let check_rhs = rhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + let check_lhs = lhs + .as_scalar() + .expect("Expected bitwise operands to be scalar") + .to_f64(); + + if check_rhs.fract() != 0. || check_lhs.fract() != 0. { + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } + let result = Ok(lhs >> rhs); self.push_quantity(result.map_err(RuntimeError::QuantityError)?); @@ -870,7 +940,7 @@ impl Vm { .to_f64(); if check_rhs.fract() != 0. { - return Err(Box::new(RuntimeError::BitwiseNotOfNonInteger)); + return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); } self.push_quantity(!rhs); } From 4d1ba9d701e4c228448023c36a94f33e9cad4053 Mon Sep 17 00:00:00 2001 From: Ryan Helminen Date: Sun, 20 Jul 2025 23:30:56 -0400 Subject: [PATCH 09/10] Implement additional error checking. Overflows during bit shift now are handled using the unbounded_shl approach. Bit shifts with negative values on the rhs of the operation also now report a runtime error. --- numbat/src/interpreter/mod.rs | 2 ++ numbat/src/number.rs | 4 ++-- numbat/src/vm.rs | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/numbat/src/interpreter/mod.rs b/numbat/src/interpreter/mod.rs index 0388e6c7e..96d215686 100644 --- a/numbat/src/interpreter/mod.rs +++ b/numbat/src/interpreter/mod.rs @@ -29,6 +29,8 @@ pub enum RuntimeError { FactorialOfNonInteger, #[error("Expected bitwise argument(s) to be a finite integer number")] BitwiseOperationOfNonInteger, + #[error("Expected rhs of bitwise shift to be positive integer number")] + BitwiseShiftOfNegativeInteger, #[error("{0}")] UnitRegistryError(UnitRegistryError), // TODO: can this even be triggered? #[error("{0}")] diff --git a/numbat/src/number.rs b/numbat/src/number.rs index 884eed837..48e01c7b0 100644 --- a/numbat/src/number.rs +++ b/numbat/src/number.rs @@ -164,7 +164,7 @@ impl std::ops::Shl for Number { type Output = Number; fn shl(self, rhs: Self) -> Self::Output { - Number(((self.0 as i64) << rhs.0 as i64) as f64) + Number((self.0 as i64).unbounded_shl(rhs.0 as u32) as f64) } } @@ -172,7 +172,7 @@ impl std::ops::Shr for Number { type Output = Number; fn shr(self, rhs: Self) -> Self::Output { - Number(((self.0 as i64) >> (rhs.0 as i64)) as f64) + Number((self.0 as i64).unbounded_shr(rhs.0 as u32) as f64) } } diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index 857ffdcd9..2f7690ecf 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -899,6 +899,8 @@ impl Vm { if check_rhs.fract() != 0. || check_lhs.fract() != 0. { return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } else if check_rhs < 0. { + return Err(Box::new(RuntimeError::BitwiseShiftOfNegativeInteger)); } let result = Ok(lhs << rhs); @@ -921,6 +923,8 @@ impl Vm { if check_rhs.fract() != 0. || check_lhs.fract() != 0. { return Err(Box::new(RuntimeError::BitwiseOperationOfNonInteger)); + } else if check_rhs < 0. { + return Err(Box::new(RuntimeError::BitwiseShiftOfNegativeInteger)); } let result = Ok(lhs >> rhs); From a503b2b925b85e6dbd6c7f11b2f1be17c896b219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20M=C3=B8ller=20Jensen?= Date: Wed, 30 Jul 2025 21:44:11 +0200 Subject: [PATCH 10/10] Add skeleton ffi function implementation --- book/src/list-functions-math.md | 32 +++++++++++++++++++++++++++++++ numbat/modules/core/functions.nbt | 10 ++++++++++ numbat/src/ffi/functions.rs | 2 ++ numbat/src/ffi/math.rs | 23 ++++++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/book/src/list-functions-math.md b/book/src/list-functions-math.md index 20b518c9f..91b999d34 100644 --- a/book/src/list-functions-math.md +++ b/book/src/list-functions-math.md @@ -321,6 +321,38 @@ fn mod(a: T, b: T) -> T +### `wrapped_shl` (Bitwise left shift) + +```nbt +fn wrapped_shl(lhs: Scalar, rhs: Scalar) -> Scalar +``` + +
+Examples + +
wrapped_shl(0b0000_0101, 4) -> bin + + = "0b1010000" [String] +
+ +
+ +### `wrapped_shr` (Bitwise right shift) + +```nbt +fn wrapped_shr(lhs: Scalar, rhs: Scalar) -> Scalar +``` + +
+Examples + +
wrapped_shr(0b0101_0000, 4) -> bin + + = "0b101" [String] +
+ +
+ ## Transcendental functions Defined in: `math::transcendental` diff --git a/numbat/modules/core/functions.nbt b/numbat/modules/core/functions.nbt index 8dfdbde3e..6db0a36b0 100644 --- a/numbat/modules/core/functions.nbt +++ b/numbat/modules/core/functions.nbt @@ -95,3 +95,13 @@ fn fract(x: Scalar) -> Scalar @url("https://doc.rust-lang.org/std/primitive.f64.html#method.rem_euclid") @example("mod(27, 5)") fn mod(a: T, b: T) -> T + +#TODO: Add description and URL +@name("Bitwise left shift") +@example("wrapped_shl(0b0000_0101, 4) -> bin") +fn wrapped_shl(lhs: Scalar, rhs: Scalar) -> Scalar + +#TODO: Add description and URL +@name("Bitwise right shift") +@example("wrapped_shr(0b0101_0000, 4) -> bin") +fn wrapped_shr(lhs: Scalar, rhs: Scalar) -> Scalar diff --git a/numbat/src/ffi/functions.rs b/numbat/src/ffi/functions.rs index d3da47e11..7cbf48b39 100644 --- a/numbat/src/ffi/functions.rs +++ b/numbat/src/ffi/functions.rs @@ -45,6 +45,8 @@ pub(crate) fn functions() -> &'static HashMap<&'static str, ForeignFunction> { // Math insert_function!("mod", mod_, 2..=2); + insert_function!(wrapped_shl, 2..=2); + insert_function!(wrapped_shr, 2..=2); insert_function!(abs, 1..=1); insert_function!(round, 1..=1); diff --git a/numbat/src/ffi/math.rs b/numbat/src/ffi/math.rs index 68c4449f2..857eb7acd 100644 --- a/numbat/src/ffi/math.rs +++ b/numbat/src/ffi/math.rs @@ -1,3 +1,8 @@ + +use std::ops::{Shl, Shr}; + + + use super::macros::*; use super::Args; use super::Result; @@ -15,6 +20,24 @@ pub fn mod_(mut args: Args) -> Result { return_quantity!(x_value.rem_euclid(y_value), x.unit().clone()) } +pub fn wrapped_shl(mut args: Args) -> Result { + let lhs = scalar_arg!(args); + let rhs = scalar_arg!(args); + + let result = lhs.shl(rhs).to_f64(); //TODO: Implement wrapped left shift + + return_scalar!(result) +} + +pub fn wrapped_shr(mut args: Args) -> Result { + let lhs = scalar_arg!(args); + let rhs = scalar_arg!(args); + + let result = lhs.shr(rhs).to_f64(); //TODO: Implement wrapped right shift + + return_scalar!(result) +} + // A simple math function with signature 'Fn[(Scalar) -> Scalar]' macro_rules! simple_scalar_math_function { ($name:ident, $op:ident) => {