Add arithmetic expressions

This commit is contained in:
2026-06-17 23:00:14 +09:00
parent dc28cddbff
commit 3f7dd7c692
18 changed files with 6336 additions and 2757 deletions
+13
View File
@@ -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,
}
+162 -1
View File
@@ -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(
+41 -2
View File
@@ -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::*;
+53 -2
View File
@@ -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,