Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions book/src/example-numbat_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions book/src/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,20 @@ 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` |
| subtraction | `x - y` |
| 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' | <code>x &#124; y</code> |
| logical 'or' | <code>x &#124;&#124; y</code> |
| unit conversion | `x -> y`, `x → y`, `x ➞ y`, `x to y` |
| conditionals | `if x then y else z` |
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions examples/numbat_syntax.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion examples/parse_error/unexpected_character.nbt

This file was deleted.

1 change: 1 addition & 0 deletions examples/runtime_error/nonscalar_bitwise_not.nbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
~2.2
36 changes: 36 additions & 0 deletions examples/tests/bitwise.nbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 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)

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)

# bitshift right checks
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)
11 changes: 11 additions & 0 deletions numbat/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub enum UnaryOperator {
Factorial(NonZeroUsize),
Negate,
LogicalNeg,
BitwiseNot,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -34,6 +35,11 @@ pub enum BinaryOperator {
NotEqual,
LogicalAnd,
LogicalOr,
BitwiseOr,
BitwiseAnd,
BitwiseXor,
BitShiftLeft,
BitShiftRight,
}

impl PrettyPrint for BinaryOperator {
Expand All @@ -55,6 +61,11 @@ impl PrettyPrint for BinaryOperator {
NotEqual => "≠",
LogicalAnd => "&&",
LogicalOr => "||",
BitwiseOr => "|",
BitwiseAnd => "&",
BitwiseXor => "⨁",
BitShiftLeft => "<<",
BitShiftRight => ">>",
});

match self {
Expand Down
9 changes: 9 additions & 0 deletions numbat/src/bytecode_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
3 changes: 2 additions & 1 deletion numbat/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_}"))])
Expand Down
2 changes: 2 additions & 0 deletions numbat/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}")]
Expand Down
1 change: 1 addition & 0 deletions numbat/src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub const KEYWORDS: &[&str] = &[
"unit ",
"use ",
"struct ",
"xor ",
// 'inline' keywords
"long",
"short",
Expand Down
48 changes: 48 additions & 0 deletions numbat/src/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this!

I believe we need to write a few more test cases for extreme inputs, and we need to improve error handling here. These bitwise operations can overflow, and we don't want Numbat to crash in those cases (but rather emit some error, similar to the division-by-zero error). I'm also not sure about these as casts...

For example:

>>> 1 << 63

  1 << 63

    = -9.22337e+18

>>> 1 << 64

thread 'main' panicked at numbat/src/number.rs:167:16:
attempt to shift left with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also not sure about these as casts...

What aspect of them are you unsure about? It was the only way I could figure out how to work around the fact that the underlying primitive type for Number is f64. None of the bitwise operations make sense in the context of an f64 type.

Good point on the overflowing. Looks like there are a bunch of different options that have different behavior defined for overflows that could occur during the shifting operations:

  • unbounded: Any overflowing bits are dropped. If the rhs is larger than the type being shifted then all the bits are dropped resulting in a value of 0.
  • wrapping: Masks off the higher order rhs bits that would cause an overflow to occur.
  • checked: Returns None if the size of the rhs is larger than the size of the type being shifted.

My preference is the unbounded implementation, but I could be convinced that it would be better to throw an error rather than to silently fail in a defined way.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be worth having more than one implementation, but I agree with preferring the unbounded version for the operator. The other versions could potentially be added as ffi functions to give the option if needed.

Disclaimer: I don't use bit shifting that much, so my preference for unbounded shifts is mostly just that they make more intuitive sense to me.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One side effect of the type casts is that it silently truncates the decimal part of any floats that someone tries to perform a bitwise oepration on. For example 5.5 | 2.1 silently truncates the .5 and .1 and gives the value of 7. I can see this being somewhat undesired. We could probably add some error checking that only allows bitwise operations to be performed when both arguments are scalars. I've already added this error checking for performing the bitwise not. I don't think it would be a big step to add it to the rest of the operations.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it might be worth having more than one implementation, but I agree with preferring the unbounded version for the operator. The other versions could potentially be added as ffi functions to give the option if needed.

I would definitely need some help with the ffi function implementation. These updates are my first forays into the world of rust, so I've mostly been pattern matching off of existing code to implement them.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, another point on the type casts is that it doesn't make sense to bit shift with a negative integer on the right-hand side of the operation. Rust throws an error in this scenario. I can fix that tonight.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One side effect of the type casts is that it silently truncates the decimal part of any floats that someone tries to perform a bitwise oepration on. For example 5.5 | 2.1 silently truncates the .5 and .1 and gives the value of 7.

Yeah that would not be ideal.

We could probably add some error checking that only allows bitwise operations to be performed when both arguments are scalars.

This sounds like a good idea to me, but I think this decision should be up to @sharkdp.

I would definitely need some help with the ffi function implementation.

No problem, I think I can help with that. I’m not sure when I can, but I'll drop some updates with the ffi connections and a todo for where the function implementation should go if you want it.

These updates are my first forays into the world of rust, so I've mostly been pattern matching off of existing code to implement them.

I’m learning rust in much the same way. Thank you for taking on the challenge to add this feature 😄

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a very simple ffi outline that should work for the left and right shifts. I've left comments with what still needs to be done before it's done.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably add some error checking that only allows bitwise operations to be performed when both arguments are scalars.

This sounds like a good idea to me, but I think this decision should be up to @sharkdp.

Yes, I think we should do that. Only scalars should be allowed to be xored. At the moment, something like 1 m xor 2 m leads to a panic.

}
}

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;

Expand All @@ -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<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(Number::from_f64(1.0), |acc, n| acc * n)
Expand Down
56 changes: 56 additions & 0 deletions numbat/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expression<'a>> {
self.parse_binop(
tokens,
&[TokenKind::BitwiseOr],
|_| BinaryOperator::BitwiseOr,
|parser| parser.logical_and(tokens),
)
}
Expand All @@ -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<Expression<'a>> {
self.parse_binop(
tokens,
&[TokenKind::BitwiseAnd],
|_| BinaryOperator::BitwiseAnd,
|parser| parser.bitwise_xor(tokens),
)
}

fn bitwise_xor(&mut self, tokens: &[Token<'a>]) -> Result<Expression<'a>> {
self.parse_binop(
tokens,
&[TokenKind::BitwiseXor],
|_| BinaryOperator::BitwiseXor,
|parser| parser.bitshift_left(tokens),
)
}

fn bitshift_left(&mut self, tokens: &[Token<'a>]) -> Result<Expression<'a>> {
self.parse_binop(
tokens,
&[TokenKind::BitShiftLeft],
|_| BinaryOperator::BitShiftLeft,
|parser| parser.bitshift_right(tokens),
)
}

fn bitshift_right(&mut self, tokens: &[Token<'a>]) -> Result<Expression<'a>> {
self.parse_binop(
tokens,
&[TokenKind::BitShiftRight],
|_| BinaryOperator::BitShiftRight,
|parser| parser.logical_neg(tokens),
)
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -3189,6 +3243,8 @@ mod tests {
"###);
}

// TODO: Add test for bitwise operations

#[test]
fn logical_operation() {
// basic
Expand Down
48 changes: 48 additions & 0 deletions numbat/src/quantity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down
Loading
Loading