Add arithmetic expressions

This commit is contained in:
Keisuke Hirata 2026-06-17 23:00:14 +09:00
parent dc28cddbff
commit 3f7dd7c692
No known key found for this signature in database
18 changed files with 6336 additions and 2757 deletions

View File

@ -71,6 +71,10 @@ pub enum Expr {
scrutinee: ExprId, scrutinee: ExprId,
arms: Vec<MatchArm>, arms: Vec<MatchArm>,
}, },
Unary {
op: UnaryOp,
expr: ExprId,
},
Binary { Binary {
op: BinaryOp, op: BinaryOp,
lhs: ExprId, lhs: ExprId,
@ -117,8 +121,17 @@ pub enum Literal {
Bool(bool), Bool(bool),
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryOp {
Neg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp { pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
And, And,
Patch, Patch,
} }

View File

@ -2,7 +2,7 @@ use alloc::{format, string::String, vec, vec::Vec};
use crate::{ use crate::{
ExprId, SourceForm, SourceId, Span, ExprId, SourceForm, SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal}, ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
constraints::normalize_constraints, constraints::normalize_constraints,
diagnostic::{Diagnostic, DiagnosticKind, Result}, diagnostic::{Diagnostic, DiagnosticKind, Result},
embedding::HostValue, embedding::HostValue,
@ -337,6 +337,18 @@ impl<L: SourceLoader> Engine<L> {
"no match arm matched", "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 } => { Expr::Binary { op, lhs, rhs } => {
let lhs = self.eval_expr( let lhs = self.eval_expr(
ExprRef { ExprRef {
@ -353,6 +365,10 @@ impl<L: SourceLoader> Engine<L> {
env, env,
)?; )?;
match op { 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::And => self.compose_and(lhs, rhs, span),
BinaryOp::Patch => self.patch(lhs, rhs), 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 { fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
match (value, expected) { match (value, expected) {
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => { (RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
@ -1072,6 +1198,41 @@ mod tests {
assert!(matches!(data, Data::Object(_))); 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] #[test]
fn composes_schema_and_value() { fn composes_schema_and_value() {
let data = eval_data( let data = eval_data(

View File

@ -36,6 +36,10 @@ pub enum TokenKind {
Equal, Equal,
Arrow, Arrow,
Amp, Amp,
Plus,
Minus,
Star,
Slash,
SlashSlash, SlashSlash,
Gt, Gt,
Gte, Gte,
@ -67,9 +71,13 @@ impl<'a> Lexer<'a> {
pub fn tokenize(mut self) -> Result<Vec<Token>> { pub fn tokenize(mut self) -> Result<Vec<Token>> {
let mut tokens = Vec::new(); let mut tokens = Vec::new();
let mut previous = None;
loop { loop {
let token = self.next_token()?; let token = self.next_token(previous.as_ref())?;
let is_eof = token.kind == TokenKind::Eof; let is_eof = token.kind == TokenKind::Eof;
if !is_eof {
previous = Some(token.kind.clone());
}
tokens.push(token); tokens.push(token);
if is_eof { if is_eof {
return Ok(tokens); 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(); self.skip_ws_and_comments();
let start = self.pos; let start = self.pos;
let Some(ch) = self.peek() else { let Some(ch) = self.peek() else {
@ -136,6 +144,18 @@ impl<'a> Lexer<'a> {
self.pos += 1; self.pos += 1;
TokenKind::Amp TokenKind::Amp
} }
b'+' => {
self.pos += 1;
TokenKind::Plus
}
b'-' => {
self.pos += 1;
TokenKind::Minus
}
b'*' => {
self.pos += 1;
TokenKind::Star
}
b'=' => { b'=' => {
self.pos += 1; self.pos += 1;
if self.consume(b'>') { if self.consume(b'>') {
@ -164,6 +184,8 @@ impl<'a> Lexer<'a> {
self.pos += 1; self.pos += 1;
if self.consume(b'/') { if self.consume(b'/') {
TokenKind::SlashSlash TokenKind::SlashSlash
} else if previous.is_some_and(token_can_end_expr) {
TokenKind::Slash
} else { } else {
self.lex_regex(start)? self.lex_regex(start)?
} }
@ -341,6 +363,23 @@ fn is_ident_continue(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_' 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec};
use crate::{ use crate::{
SourceId, Span, 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}, diagnostic::{Diagnostic, Result},
lexer::{Lexer, Token, TokenKind}, lexer::{Lexer, Token, TokenKind},
}; };
@ -115,6 +115,38 @@ impl Parser {
let rhs = self.parse_expr(r_bp)?; let rhs = self.parse_expr(r_bp)?;
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span); let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
lhs = match kind { 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( InfixKind::And => self.ast.push(
Expr::Binary { Expr::Binary {
op: BinaryOp::And, op: BinaryOp::And,
@ -173,6 +205,17 @@ impl Parser {
TokenKind::Let => self.parse_let(token.span), TokenKind::Let => self.parse_let(token.span),
TokenKind::Match => self.parse_match(token.span), TokenKind::Match => self.parse_match(token.span),
TokenKind::Import => self.parse_import(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 => { TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
let op = match token.kind { let op = match token.kind {
TokenKind::Gt => CompareOp::Gt, TokenKind::Gt => CompareOp::Gt,
@ -181,7 +224,7 @@ impl Parser {
TokenKind::Lte => CompareOp::Lte, TokenKind::Lte => CompareOp::Lte,
_ => unreachable!(), _ => unreachable!(),
}; };
let value = self.parse_expr(8)?; let value = self.parse_expr(6)?;
let span = token.span.join(self.ast.span(value)); let span = token.span.join(self.ast.span(value));
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span)) Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
} }
@ -406,6 +449,10 @@ impl Parser {
TokenKind::Default => Some((InfixKind::Default, 1, 2)), TokenKind::Default => Some((InfixKind::Default, 1, 2)),
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)), TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
TokenKind::Amp => Some((InfixKind::And, 5, 6)), 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, _ => None,
} }
} }
@ -498,6 +545,10 @@ impl Parser {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum InfixKind { enum InfixKind {
Add,
Sub,
Mul,
Div,
And, And,
Patch, Patch,
Default, Default,

View File

@ -0,0 +1,43 @@
# Arithmetic Expression
Decodal supports arithmetic over concrete numeric values.
```dcdl
{
workers = 2 + 2;
timeout = 30.0 / 2;
port = 8000 + 80;
negative = -1;
}
```
## Operators
- `+` addition
- `-` subtraction
- `*` multiplication
- `/` division
- unary `-` negation
`*` and `/` bind tighter than `+` and `-`.
Parentheses can be used to make grouping explicit.
```dcdl
2 + 3 * 4 # 14
(2 + 3) * 4 # 20
```
## Numeric behavior
Arithmetic requires concrete `Int` or `Float` operands.
`Int + Int`, `Int - Int`, and `Int * Int` produce `Int` when no overflow occurs.
Mixed `Int` / `Float` arithmetic produces `Float`.
Division always produces `Float`.
Division by zero and integer overflow are evaluation errors.
Arithmetic expressions can be used anywhere a concrete numeric expression is expected, including defaults and numeric constraints.
```dcdl
port = Int & > 4000 + 42 default 8080;
```

View File

@ -1,6 +1,26 @@
# 合成演算子 # 演算子
この章では、`&` と `//` の意味を定義する。 この章では、Decodal の演算子の意味を定義する。
## 優先順位
優先順位は高い順に以下である。
1. 関数呼び出しとフィールド参照
2. unary `-`
3. `*` `/`
4. `+` `-`
5. `&`
6. `//`
7. `default`
同じ優先順位の二項演算子は左結合である。
`default` は右結合である。
## Arithmetic operators
`+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。
詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。
## `&`: 制約合成 ## `&`: 制約合成

View File

@ -115,6 +115,7 @@ rec
主要な演算子は以下である。 主要な演算子は以下である。
```text ```text
+ - * / 四則演算
& 制約合成 & 制約合成
// patch 合成 // patch 合成
default fallback 指定 default fallback 指定
@ -122,5 +123,4 @@ default fallback 指定
. フィールド参照 / ドットパス定義 . フィールド参照 / ドットパス定義
``` ```
演算子の優先順位は未確定である。 演算子の優先順位は [合成演算子](./operators.md) で定義する。
詳細は [合成演算子](./operators.md) で定義する。

View File

@ -74,3 +74,31 @@ base // {
body: (literal (string))) body: (literal (string)))
(match_arm (match_arm
body: (literal (string)))))))) body: (literal (string))))))))
==================
Arithmetic
==================
{
value = 1 + 2 * 3;
grouped = (1 + 2) / -3;
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (literal (integer))
right: (binary_expression
left: (literal (integer))
right: (literal (integer)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (parenthesized_expression
(binary_expression
left: (literal (integer))
right: (literal (integer))))
right: (unary_expression
operand: (literal (integer)))))))

View File

@ -2,6 +2,9 @@ const PREC = {
DEFAULT: 1, DEFAULT: 1,
PATCH: 2, PATCH: 2,
AND: 3, AND: 3,
ADD: 4,
MUL: 5,
UNARY: 6,
CALL: 7, CALL: 7,
PATH: 8, PATH: 8,
}; };
@ -52,6 +55,7 @@ module.exports = grammar({
$.parenthesized_expression, $.parenthesized_expression,
$.call_expression, $.call_expression,
$.path_expression, $.path_expression,
$.unary_expression,
$.binary_expression, $.binary_expression,
$.default_expression, $.default_expression,
), ),
@ -161,10 +165,25 @@ module.exports = grammar({
comparison_constraint: $ => prec(6, seq( comparison_constraint: $ => prec(6, seq(
field('operator', choice('>', '>=', '<', '<=')), field('operator', choice('>', '>=', '<', '<=')),
field('value', choice($.integer, $.float)), field('value', $._expression),
)),
unary_expression: $ => prec(PREC.UNARY, seq(
field('operator', '-'),
field('operand', $._expression),
)), )),
binary_expression: $ => choice( binary_expression: $ => choice(
prec.left(PREC.ADD, seq(
field('left', $._expression),
field('operator', choice('+', '-')),
field('right', $._expression),
)),
prec.left(PREC.MUL, seq(
field('left', $._expression),
field('operator', choice('*', '/')),
field('right', $._expression),
)),
prec.left(PREC.AND, seq( prec.left(PREC.AND, seq(
field('left', $._expression), field('left', $._expression),
field('operator', '&'), field('operator', '&'),

View File

@ -17,6 +17,10 @@
[ [
"&" "&"
"//" "//"
"+"
"-"
"*"
"/"
"=>" "=>"
"=" "="
">" ">"

View File

@ -127,6 +127,10 @@
"type": "SYMBOL", "type": "SYMBOL",
"name": "path_expression" "name": "path_expression"
}, },
{
"type": "SYMBOL",
"name": "unary_expression"
},
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "binary_expression" "name": "binary_expression"
@ -844,17 +848,33 @@
"type": "FIELD", "type": "FIELD",
"name": "value", "name": "value",
"content": { "content": {
"type": "CHOICE", "type": "SYMBOL",
"members": [ "name": "_expression"
{ }
"type": "SYMBOL", }
"name": "integer" ]
}, }
{ },
"type": "SYMBOL", "unary_expression": {
"name": "float" "type": "PREC",
} "value": 6,
] "content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "STRING",
"value": "-"
}
},
{
"type": "FIELD",
"name": "operand",
"content": {
"type": "SYMBOL",
"name": "_expression"
} }
} }
] ]
@ -863,6 +883,90 @@
"binary_expression": { "binary_expression": {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
{
"type": "PREC_LEFT",
"value": 4,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "+"
},
{
"type": "STRING",
"value": "-"
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 5,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "*"
},
{
"type": "STRING",
"value": "/"
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{ {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 3, "value": 3,

View File

@ -66,6 +66,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -137,6 +141,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
}, },
@ -148,6 +156,22 @@
"type": "&", "type": "&",
"named": false "named": false
}, },
{
"type": "*",
"named": false
},
{
"type": "+",
"named": false
},
{
"type": "-",
"named": false
},
{
"type": "/",
"named": false
},
{ {
"type": "//", "type": "//",
"named": false "named": false
@ -217,6 +241,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -294,6 +322,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -361,6 +393,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -396,11 +432,67 @@
"required": true, "required": true,
"types": [ "types": [
{ {
"type": "float", "type": "array",
"named": true "named": true
}, },
{ {
"type": "integer", "type": "binary_expression",
"named": true
},
{
"type": "call_expression",
"named": true
},
{
"type": "comparison_constraint",
"named": true
},
{
"type": "default_expression",
"named": true
},
{
"type": "function_expression",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "import_expression",
"named": true
},
{
"type": "let_expression",
"named": true
},
{
"type": "literal",
"named": true
},
{
"type": "match_expression",
"named": true
},
{
"type": "object",
"named": true
},
{
"type": "parenthesized_expression",
"named": true
},
{
"type": "path_expression",
"named": true
},
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true "named": true
} }
] ]
@ -474,6 +566,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
}, },
@ -540,6 +636,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -622,6 +722,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -709,6 +813,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -807,6 +915,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -916,6 +1028,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
}, },
@ -986,6 +1102,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -1058,6 +1178,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -1155,6 +1279,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
}, },
@ -1237,6 +1365,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -1318,6 +1450,10 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
@ -1394,10 +1530,100 @@
{ {
"type": "regex_literal", "type": "regex_literal",
"named": true "named": true
},
{
"type": "unary_expression",
"named": true
} }
] ]
} }
}, },
{
"type": "unary_expression",
"named": true,
"fields": {
"operand": {
"multiple": false,
"required": true,
"types": [
{
"type": "array",
"named": true
},
{
"type": "binary_expression",
"named": true
},
{
"type": "call_expression",
"named": true
},
{
"type": "comparison_constraint",
"named": true
},
{
"type": "default_expression",
"named": true
},
{
"type": "function_expression",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "import_expression",
"named": true
},
{
"type": "let_expression",
"named": true
},
{
"type": "literal",
"named": true
},
{
"type": "match_expression",
"named": true
},
{
"type": "object",
"named": true
},
{
"type": "parenthesized_expression",
"named": true
},
{
"type": "path_expression",
"named": true
},
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
},
"operator": {
"multiple": false,
"required": true,
"types": [
{
"type": "-",
"named": false
}
]
}
}
},
{ {
"type": "&", "type": "&",
"named": false "named": false
@ -1410,14 +1636,30 @@
"type": ")", "type": ")",
"named": false "named": false
}, },
{
"type": "*",
"named": false
},
{
"type": "+",
"named": false
},
{ {
"type": ",", "type": ",",
"named": false "named": false
}, },
{
"type": "-",
"named": false
},
{ {
"type": ".", "type": ".",
"named": false "named": false
}, },
{
"type": "/",
"named": false
},
{ {
"type": "//", "type": "//",
"named": false "named": false

File diff suppressed because it is too large Load Diff

7
examples/arithmetic.dcdl Normal file
View File

@ -0,0 +1,7 @@
{
workers = 2 + 2;
memory_gib = 1.5 * 4;
port = 9000 + 443;
timeout_seconds = 30 / 2;
negative_offset = -3;
}

View File

@ -50,6 +50,7 @@ export const nav = [
{ title: 'Import', slug: 'language/expression/import' }, { title: 'Import', slug: 'language/expression/import' },
{ title: 'Composition', slug: 'language/expression/composition' }, { title: 'Composition', slug: 'language/expression/composition' },
{ title: 'Default', slug: 'language/expression/default' }, { title: 'Default', slug: 'language/expression/default' },
{ title: 'Arithmetic', slug: 'language/expression/arithmetic' },
{ title: 'String Interpolation', slug: 'language/expression/string-interpolation' }, { title: 'String Interpolation', slug: 'language/expression/string-interpolation' },
], ],
}, },

View File

@ -28,12 +28,13 @@ export function highlightCode(code, language = '') {
export function highlightDecodal(source) { export function highlightDecodal(source) {
let html = ''; let html = '';
let index = 0; let index = 0;
let canEndExpression = false;
while (index < source.length) { while (index < source.length) {
const char = source[index]; const char = source[index];
const next = source[index + 1]; const next = source[index + 1];
if (char === '/' && next === '/') { if (char === '#') {
const end = readUntilLineEnd(source, index); const end = readUntilLineEnd(source, index);
html += token('comment', source.slice(index, end)); html += token('comment', source.slice(index, end));
index = end; index = end;
@ -44,13 +45,15 @@ export function highlightDecodal(source) {
const end = readString(source, index); const end = readString(source, index);
html += token('string', source.slice(index, end)); html += token('string', source.slice(index, end));
index = end; index = end;
canEndExpression = true;
continue; continue;
} }
if (char === '/' && next && next !== '/') { if (char === '/' && next && next !== '/' && !canEndExpression) {
const end = readRegex(source, index); const end = readRegex(source, index);
html += token('regex', source.slice(index, end)); html += token('regex', source.slice(index, end));
index = end; index = end;
canEndExpression = true;
continue; continue;
} }
@ -58,6 +61,7 @@ export function highlightDecodal(source) {
const end = readNumber(source, index); const end = readNumber(source, index);
html += token('number', source.slice(index, end)); html += token('number', source.slice(index, end));
index = end; index = end;
canEndExpression = true;
continue; continue;
} }
@ -68,6 +72,7 @@ export function highlightDecodal(source) {
else if (DECODAL_TYPES.has(ident)) html += token('type', ident); else if (DECODAL_TYPES.has(ident)) html += token('type', ident);
else if (DECODAL_LITERALS.has(ident)) html += token('literal', ident); else if (DECODAL_LITERALS.has(ident)) html += token('literal', ident);
else html += escapeHtml(ident); else html += escapeHtml(ident);
canEndExpression = true;
index = end; index = end;
continue; continue;
} }
@ -75,6 +80,7 @@ export function highlightDecodal(source) {
if (isOperatorStart(char)) { if (isOperatorStart(char)) {
const end = readOperator(source, index); const end = readOperator(source, index);
html += token('operator', source.slice(index, end)); html += token('operator', source.slice(index, end));
canEndExpression = /[})\]]/.test(char);
index = end; index = end;
continue; continue;
} }
@ -184,11 +190,12 @@ function readIdentifier(source, start) {
} }
function isOperatorStart(char) { function isOperatorStart(char) {
return /[&=<>!|:;,.{}()[\]]/.test(char ?? ''); return '&=<>!|:;,.{}()[]+-*/'.includes(char ?? '');
} }
function readOperator(source, start) { function readOperator(source, start) {
let index = start + 1; let index = start + 1;
if (source[start] === '/' && source[index] === '/') return index + 1;
if ((source[start] === '<' || source[start] === '>' || source[start] === '=' || source[start] === '!') && source[index] === '=') { if ((source[start] === '<' || source[start] === '>' || source[start] === '=' || source[start] === '!') && source[index] === '=') {
index += 1; index += 1;
} }

View File

@ -9,7 +9,7 @@ const starterFiles = {
in in
schema.Service & { schema.Service & {
name = "api"; name = "api";
port = 9443; port = 9000 + 443;
} }
`, `,
'schemas/service.dcdl': `Service = { 'schemas/service.dcdl': `Service = {