Add arithmetic expressions
This commit is contained in:
@@ -71,6 +71,10 @@ pub enum Expr {
|
||||
scrutinee: ExprId,
|
||||
arms: Vec<MatchArm>,
|
||||
},
|
||||
Unary {
|
||||
op: UnaryOp,
|
||||
expr: ExprId,
|
||||
},
|
||||
Binary {
|
||||
op: BinaryOp,
|
||||
lhs: ExprId,
|
||||
@@ -117,8 +121,17 @@ pub enum Literal {
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum UnaryOp {
|
||||
Neg,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BinaryOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
And,
|
||||
Patch,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use alloc::{format, string::String, vec, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
ExprId, SourceForm, SourceId, Span,
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal},
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
|
||||
constraints::normalize_constraints,
|
||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||
embedding::HostValue,
|
||||
@@ -337,6 +337,18 @@ impl<L: SourceLoader> Engine<L> {
|
||||
"no match arm matched",
|
||||
))
|
||||
}
|
||||
Expr::Unary { op, expr } => {
|
||||
let value = self.eval_expr(
|
||||
ExprRef {
|
||||
module: reference.module,
|
||||
expr,
|
||||
},
|
||||
env,
|
||||
)?;
|
||||
match op {
|
||||
UnaryOp::Neg => negate_number(value, span),
|
||||
}
|
||||
}
|
||||
Expr::Binary { op, lhs, rhs } => {
|
||||
let lhs = self.eval_expr(
|
||||
ExprRef {
|
||||
@@ -353,6 +365,10 @@ impl<L: SourceLoader> Engine<L> {
|
||||
env,
|
||||
)?;
|
||||
match op {
|
||||
BinaryOp::Add => arithmetic(lhs, rhs, ArithmeticOp::Add, span),
|
||||
BinaryOp::Sub => arithmetic(lhs, rhs, ArithmeticOp::Sub, span),
|
||||
BinaryOp::Mul => arithmetic(lhs, rhs, ArithmeticOp::Mul, span),
|
||||
BinaryOp::Div => arithmetic(lhs, rhs, ArithmeticOp::Div, span),
|
||||
BinaryOp::And => self.compose_and(lhs, rhs, span),
|
||||
BinaryOp::Patch => self.patch(lhs, rhs),
|
||||
}
|
||||
@@ -999,6 +1015,116 @@ fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ArithmeticOp {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Number {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
fn negate_number(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
|
||||
match value {
|
||||
RuntimeValue::Concrete(ConcreteValue::Int(value)) => value
|
||||
.checked_neg()
|
||||
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
||||
.ok_or_else(|| arithmetic_error(span, "integer negation overflow")),
|
||||
RuntimeValue::Concrete(ConcreteValue::Float(value)) => {
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Float(-value)))
|
||||
}
|
||||
_ => Err(Diagnostic::new(
|
||||
DiagnosticKind::TypeMismatch,
|
||||
span,
|
||||
"unary '-' expects a numeric value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn arithmetic(
|
||||
lhs: RuntimeValue,
|
||||
rhs: RuntimeValue,
|
||||
op: ArithmeticOp,
|
||||
span: Span,
|
||||
) -> Result<RuntimeValue> {
|
||||
let lhs = number_from_runtime(lhs).ok_or_else(|| arithmetic_type_error(span))?;
|
||||
let rhs = number_from_runtime(rhs).ok_or_else(|| arithmetic_type_error(span))?;
|
||||
match (lhs, rhs) {
|
||||
(Number::Int(lhs), Number::Int(rhs)) => arithmetic_int(lhs, rhs, op, span),
|
||||
(lhs, rhs) => arithmetic_float(number_to_f64(lhs), number_to_f64(rhs), op, span),
|
||||
}
|
||||
}
|
||||
|
||||
fn arithmetic_int(lhs: i64, rhs: i64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
|
||||
match op {
|
||||
ArithmeticOp::Add => lhs
|
||||
.checked_add(rhs)
|
||||
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
||||
.ok_or_else(|| arithmetic_error(span, "integer addition overflow")),
|
||||
ArithmeticOp::Sub => lhs
|
||||
.checked_sub(rhs)
|
||||
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
||||
.ok_or_else(|| arithmetic_error(span, "integer subtraction overflow")),
|
||||
ArithmeticOp::Mul => lhs
|
||||
.checked_mul(rhs)
|
||||
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
|
||||
.ok_or_else(|| arithmetic_error(span, "integer multiplication overflow")),
|
||||
ArithmeticOp::Div => {
|
||||
if rhs == 0 {
|
||||
return Err(arithmetic_error(span, "division by zero"));
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Float(
|
||||
lhs as f64 / rhs as f64,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn arithmetic_float(lhs: f64, rhs: f64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
|
||||
if matches!(op, ArithmeticOp::Div) && rhs == 0.0 {
|
||||
return Err(arithmetic_error(span, "division by zero"));
|
||||
}
|
||||
let value = match op {
|
||||
ArithmeticOp::Add => lhs + rhs,
|
||||
ArithmeticOp::Sub => lhs - rhs,
|
||||
ArithmeticOp::Mul => lhs * rhs,
|
||||
ArithmeticOp::Div => lhs / rhs,
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Float(value)))
|
||||
}
|
||||
|
||||
fn number_from_runtime(value: RuntimeValue) -> Option<Number> {
|
||||
match value {
|
||||
RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(Number::Int(value)),
|
||||
RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(Number::Float(value)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn number_to_f64(value: Number) -> f64 {
|
||||
match value {
|
||||
Number::Int(value) => value as f64,
|
||||
Number::Float(value) => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn arithmetic_type_error(span: Span) -> Diagnostic {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::TypeMismatch,
|
||||
span,
|
||||
"arithmetic operators expect numeric values",
|
||||
)
|
||||
}
|
||||
|
||||
fn arithmetic_error(span: Span, message: &'static str) -> Diagnostic {
|
||||
Diagnostic::new(DiagnosticKind::Conflict, span, message)
|
||||
}
|
||||
|
||||
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
|
||||
match (value, expected) {
|
||||
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
||||
@@ -1072,6 +1198,41 @@ mod tests {
|
||||
assert!(matches!(data, Data::Object(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluates_arithmetic_expressions() {
|
||||
let data = eval_data(
|
||||
r#"
|
||||
{
|
||||
sum = 1 + 2 * 3;
|
||||
diff = 10 - 4;
|
||||
product = (2 + 3) * 4;
|
||||
quotient = 5 / 2;
|
||||
negative = -3 + 1;
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields[0].value, Data::Int(7));
|
||||
assert_eq!(fields[1].value, Data::Int(6));
|
||||
assert_eq!(fields[2].value, Data::Int(20));
|
||||
assert_eq!(fields[3].value, Data::Float(2.5));
|
||||
assert_eq!(fields[4].value, Data::Int(-2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_can_feed_constraints() {
|
||||
let data = eval_data("port = Int & > 4000 + 42 default 8080;");
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields[0].value, Data::Int(8080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_division_by_zero() {
|
||||
let parsed = parse_source("1 / 0").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
assert!(engine.eval_root().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composes_schema_and_value() {
|
||||
let data = eval_data(
|
||||
|
||||
@@ -36,6 +36,10 @@ pub enum TokenKind {
|
||||
Equal,
|
||||
Arrow,
|
||||
Amp,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
SlashSlash,
|
||||
Gt,
|
||||
Gte,
|
||||
@@ -67,9 +71,13 @@ impl<'a> Lexer<'a> {
|
||||
|
||||
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut previous = None;
|
||||
loop {
|
||||
let token = self.next_token()?;
|
||||
let token = self.next_token(previous.as_ref())?;
|
||||
let is_eof = token.kind == TokenKind::Eof;
|
||||
if !is_eof {
|
||||
previous = Some(token.kind.clone());
|
||||
}
|
||||
tokens.push(token);
|
||||
if is_eof {
|
||||
return Ok(tokens);
|
||||
@@ -77,7 +85,7 @@ impl<'a> Lexer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn next_token(&mut self) -> Result<Token> {
|
||||
fn next_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
||||
self.skip_ws_and_comments();
|
||||
let start = self.pos;
|
||||
let Some(ch) = self.peek() else {
|
||||
@@ -136,6 +144,18 @@ impl<'a> Lexer<'a> {
|
||||
self.pos += 1;
|
||||
TokenKind::Amp
|
||||
}
|
||||
b'+' => {
|
||||
self.pos += 1;
|
||||
TokenKind::Plus
|
||||
}
|
||||
b'-' => {
|
||||
self.pos += 1;
|
||||
TokenKind::Minus
|
||||
}
|
||||
b'*' => {
|
||||
self.pos += 1;
|
||||
TokenKind::Star
|
||||
}
|
||||
b'=' => {
|
||||
self.pos += 1;
|
||||
if self.consume(b'>') {
|
||||
@@ -164,6 +184,8 @@ impl<'a> Lexer<'a> {
|
||||
self.pos += 1;
|
||||
if self.consume(b'/') {
|
||||
TokenKind::SlashSlash
|
||||
} else if previous.is_some_and(token_can_end_expr) {
|
||||
TokenKind::Slash
|
||||
} else {
|
||||
self.lex_regex(start)?
|
||||
}
|
||||
@@ -341,6 +363,23 @@ fn is_ident_continue(c: u8) -> bool {
|
||||
c.is_ascii_alphanumeric() || c == b'_'
|
||||
}
|
||||
|
||||
fn token_can_end_expr(kind: &TokenKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
TokenKind::Ident(_)
|
||||
| TokenKind::Int(_)
|
||||
| TokenKind::Float(_)
|
||||
| TokenKind::String(_)
|
||||
| TokenKind::Regex(_)
|
||||
| TokenKind::True
|
||||
| TokenKind::False
|
||||
| TokenKind::Underscore
|
||||
| TokenKind::RBrace
|
||||
| TokenKind::RBracket
|
||||
| TokenKind::RParen
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
SourceId, Span,
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param},
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
|
||||
diagnostic::{Diagnostic, Result},
|
||||
lexer::{Lexer, Token, TokenKind},
|
||||
};
|
||||
@@ -115,6 +115,38 @@ impl Parser {
|
||||
let rhs = self.parse_expr(r_bp)?;
|
||||
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
|
||||
lhs = match kind {
|
||||
InfixKind::Add => self.ast.push(
|
||||
Expr::Binary {
|
||||
op: BinaryOp::Add,
|
||||
lhs,
|
||||
rhs,
|
||||
},
|
||||
span,
|
||||
),
|
||||
InfixKind::Sub => self.ast.push(
|
||||
Expr::Binary {
|
||||
op: BinaryOp::Sub,
|
||||
lhs,
|
||||
rhs,
|
||||
},
|
||||
span,
|
||||
),
|
||||
InfixKind::Mul => self.ast.push(
|
||||
Expr::Binary {
|
||||
op: BinaryOp::Mul,
|
||||
lhs,
|
||||
rhs,
|
||||
},
|
||||
span,
|
||||
),
|
||||
InfixKind::Div => self.ast.push(
|
||||
Expr::Binary {
|
||||
op: BinaryOp::Div,
|
||||
lhs,
|
||||
rhs,
|
||||
},
|
||||
span,
|
||||
),
|
||||
InfixKind::And => self.ast.push(
|
||||
Expr::Binary {
|
||||
op: BinaryOp::And,
|
||||
@@ -173,6 +205,17 @@ impl Parser {
|
||||
TokenKind::Let => self.parse_let(token.span),
|
||||
TokenKind::Match => self.parse_match(token.span),
|
||||
TokenKind::Import => self.parse_import(token.span),
|
||||
TokenKind::Minus => {
|
||||
let expr = self.parse_expr(11)?;
|
||||
let span = token.span.join(self.ast.span(expr));
|
||||
Ok(self.ast.push(
|
||||
Expr::Unary {
|
||||
op: UnaryOp::Neg,
|
||||
expr,
|
||||
},
|
||||
span,
|
||||
))
|
||||
}
|
||||
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
|
||||
let op = match token.kind {
|
||||
TokenKind::Gt => CompareOp::Gt,
|
||||
@@ -181,7 +224,7 @@ impl Parser {
|
||||
TokenKind::Lte => CompareOp::Lte,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let value = self.parse_expr(8)?;
|
||||
let value = self.parse_expr(6)?;
|
||||
let span = token.span.join(self.ast.span(value));
|
||||
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
|
||||
}
|
||||
@@ -406,6 +449,10 @@ impl Parser {
|
||||
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
||||
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
||||
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
||||
TokenKind::Plus => Some((InfixKind::Add, 7, 8)),
|
||||
TokenKind::Minus => Some((InfixKind::Sub, 7, 8)),
|
||||
TokenKind::Star => Some((InfixKind::Mul, 9, 10)),
|
||||
TokenKind::Slash => Some((InfixKind::Div, 9, 10)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -498,6 +545,10 @@ impl Parser {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum InfixKind {
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
Div,
|
||||
And,
|
||||
Patch,
|
||||
Default,
|
||||
|
||||
Reference in New Issue
Block a user