Add logical and comparison expressions

This commit is contained in:
Keisuke Hirata 2026-06-17 23:38:44 +09:00
parent 3f7dd7c692
commit 2fe54bda62
No known key found for this signature in database
18 changed files with 6627 additions and 4620 deletions

View File

@ -124,6 +124,7 @@ pub enum Literal {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryOp { pub enum UnaryOp {
Neg, Neg,
Not,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -132,6 +133,14 @@ pub enum BinaryOp {
Sub, Sub,
Mul, Mul,
Div, Div,
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
LogicalAnd,
LogicalOr,
And, And,
Patch, Patch,
} }

View File

@ -347,30 +347,95 @@ impl<L: SourceLoader> Engine<L> {
)?; )?;
match op { match op {
UnaryOp::Neg => negate_number(value, span), UnaryOp::Neg => negate_number(value, span),
UnaryOp::Not => negate_bool(value, span),
} }
} }
Expr::Binary { op, lhs, rhs } => { Expr::Binary { op, lhs, rhs } => {
let lhs = self.eval_expr( let lhs_value = self.eval_expr(
ExprRef { ExprRef {
module: reference.module, module: reference.module,
expr: lhs, expr: lhs,
}, },
env, env,
)?; )?;
let rhs = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
match op { match op {
BinaryOp::Add => arithmetic(lhs, rhs, ArithmeticOp::Add, span), BinaryOp::LogicalAnd => {
BinaryOp::Sub => arithmetic(lhs, rhs, ArithmeticOp::Sub, span), if !bool_from_runtime(lhs_value, span)? {
BinaryOp::Mul => arithmetic(lhs, rhs, ArithmeticOp::Mul, span), return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(false)));
BinaryOp::Div => arithmetic(lhs, rhs, ArithmeticOp::Div, span), }
BinaryOp::And => self.compose_and(lhs, rhs, span), let rhs_value = self.eval_expr(
BinaryOp::Patch => self.patch(lhs, rhs), ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
bool_from_runtime(rhs_value, span)?,
)))
}
BinaryOp::LogicalOr => {
if bool_from_runtime(lhs_value, span)? {
return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(true)));
}
let rhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
bool_from_runtime(rhs_value, span)?,
)))
}
_ => {
let rhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
match op {
BinaryOp::Add => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Add, span)
}
BinaryOp::Sub => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Sub, span)
}
BinaryOp::Mul => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Mul, span)
}
BinaryOp::Div => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Div, span)
}
BinaryOp::Equal => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Equal, span)
}
BinaryOp::NotEqual => {
compare_expr(lhs_value, rhs_value, CompareExprOp::NotEqual, span)
}
BinaryOp::Greater => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Greater, span)
}
BinaryOp::GreaterEqual => compare_expr(
lhs_value,
rhs_value,
CompareExprOp::GreaterEqual,
span,
),
BinaryOp::Less => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Less, span)
}
BinaryOp::LessEqual => {
compare_expr(lhs_value, rhs_value, CompareExprOp::LessEqual, span)
}
BinaryOp::And => self.compose_and(lhs_value, rhs_value, span),
BinaryOp::Patch => self.patch(lhs_value, rhs_value),
BinaryOp::LogicalAnd | BinaryOp::LogicalOr => unreachable!(),
}
}
} }
} }
Expr::Default { base, fallback } => { Expr::Default { base, fallback } => {
@ -1023,6 +1088,16 @@ enum ArithmeticOp {
Div, Div,
} }
#[derive(Debug, Clone, Copy)]
enum CompareExprOp {
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum Number { enum Number {
Int(i64), Int(i64),
@ -1046,6 +1121,93 @@ fn negate_number(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
} }
} }
fn negate_bool(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
!bool_from_runtime(value, span)?,
)))
}
fn bool_from_runtime(value: RuntimeValue, span: Span) -> Result<bool> {
match value {
RuntimeValue::Concrete(ConcreteValue::Bool(value)) => Ok(value),
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"logical operators expect boolean values",
)),
}
}
fn compare_expr(
lhs: RuntimeValue,
rhs: RuntimeValue,
op: CompareExprOp,
span: Span,
) -> Result<RuntimeValue> {
let result = match op {
CompareExprOp::Equal => scalar_equal(&lhs, &rhs, span)?,
CompareExprOp::NotEqual => !scalar_equal(&lhs, &rhs, span)?,
CompareExprOp::Greater => compare_numbers(lhs, rhs, |lhs, rhs| lhs > rhs, span)?,
CompareExprOp::GreaterEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs >= rhs, span)?,
CompareExprOp::Less => compare_numbers(lhs, rhs, |lhs, rhs| lhs < rhs, span)?,
CompareExprOp::LessEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs <= rhs, span)?,
};
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(result)))
}
fn scalar_equal(lhs: &RuntimeValue, rhs: &RuntimeValue, span: Span) -> Result<bool> {
match (lhs, rhs) {
(
RuntimeValue::Concrete(ConcreteValue::String(lhs)),
RuntimeValue::Concrete(ConcreteValue::String(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Bool(lhs)),
RuntimeValue::Concrete(ConcreteValue::Bool(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
) => Ok(*lhs as f64 == *rhs),
(
RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
) => Ok(*lhs == *rhs as f64),
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"equality operators expect comparable scalar values",
)),
}
}
fn compare_numbers(
lhs: RuntimeValue,
rhs: RuntimeValue,
compare: impl FnOnce(f64, f64) -> bool,
span: Span,
) -> Result<bool> {
let lhs = number_from_runtime(lhs).ok_or_else(|| comparison_type_error(span))?;
let rhs = number_from_runtime(rhs).ok_or_else(|| comparison_type_error(span))?;
Ok(compare(number_to_f64(lhs), number_to_f64(rhs)))
}
fn comparison_type_error(span: Span) -> Diagnostic {
Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"ordering operators expect numeric values",
)
}
fn arithmetic( fn arithmetic(
lhs: RuntimeValue, lhs: RuntimeValue,
rhs: RuntimeValue, rhs: RuntimeValue,
@ -1233,6 +1395,50 @@ mod tests {
assert!(engine.eval_root().is_err()); assert!(engine.eval_root().is_err());
} }
#[test]
fn evaluates_logical_and_comparison_expressions() {
let data = eval_data(
r#"
{
enabled = true && !false;
fallback = false || true;
compare = 9000 + 443 > 9442;
equality = 1 == 1.0;
inequality = "prod" != "dev";
}
"#,
);
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::Bool(true));
assert_eq!(fields[1].value, Data::Bool(true));
assert_eq!(fields[2].value, Data::Bool(true));
assert_eq!(fields[3].value, Data::Bool(true));
assert_eq!(fields[4].value, Data::Bool(true));
}
#[test]
fn logical_operators_short_circuit() {
let false_and_missing = eval_data("false && missing_identifier");
assert_eq!(false_and_missing, Data::Bool(false));
let true_or_missing = eval_data("true || missing_identifier");
assert_eq!(true_or_missing, Data::Bool(true));
}
#[test]
fn rejects_invalid_logical_operands() {
let parsed = parse_source("true && 1").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test]
fn rejects_invalid_comparison_operands() {
let parsed = parse_source("\"a\" < \"b\"").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

@ -34,8 +34,13 @@ pub enum TokenKind {
Dot, Dot,
Colon, Colon,
Equal, Equal,
EqualEqual,
Bang,
BangEqual,
Arrow, Arrow,
Amp, Amp,
AmpAmp,
PipePipe,
Plus, Plus,
Minus, Minus,
Star, Star,
@ -142,7 +147,22 @@ impl<'a> Lexer<'a> {
} }
b'&' => { b'&' => {
self.pos += 1; self.pos += 1;
TokenKind::Amp if self.consume(b'&') {
TokenKind::AmpAmp
} else {
TokenKind::Amp
}
}
b'|' => {
self.pos += 1;
if self.consume(b'|') {
TokenKind::PipePipe
} else {
return Err(Diagnostic::syntax(
self.span(start, self.pos),
"expected '|' after '|'",
));
}
} }
b'+' => { b'+' => {
self.pos += 1; self.pos += 1;
@ -160,10 +180,20 @@ impl<'a> Lexer<'a> {
self.pos += 1; self.pos += 1;
if self.consume(b'>') { if self.consume(b'>') {
TokenKind::Arrow TokenKind::Arrow
} else if self.consume(b'=') {
TokenKind::EqualEqual
} else { } else {
TokenKind::Equal TokenKind::Equal
} }
} }
b'!' => {
self.pos += 1;
if self.consume(b'=') {
TokenKind::BangEqual
} else {
TokenKind::Bang
}
}
b'>' => { b'>' => {
self.pos += 1; self.pos += 1;
if self.consume(b'=') { if self.consume(b'=') {

View File

@ -147,6 +147,70 @@ impl Parser {
}, },
span, span,
), ),
InfixKind::Equal => self.ast.push(
Expr::Binary {
op: BinaryOp::Equal,
lhs,
rhs,
},
span,
),
InfixKind::NotEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::NotEqual,
lhs,
rhs,
},
span,
),
InfixKind::Greater => self.ast.push(
Expr::Binary {
op: BinaryOp::Greater,
lhs,
rhs,
},
span,
),
InfixKind::GreaterEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::GreaterEqual,
lhs,
rhs,
},
span,
),
InfixKind::Less => self.ast.push(
Expr::Binary {
op: BinaryOp::Less,
lhs,
rhs,
},
span,
),
InfixKind::LessEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::LessEqual,
lhs,
rhs,
},
span,
),
InfixKind::LogicalAnd => self.ast.push(
Expr::Binary {
op: BinaryOp::LogicalAnd,
lhs,
rhs,
},
span,
),
InfixKind::LogicalOr => self.ast.push(
Expr::Binary {
op: BinaryOp::LogicalOr,
lhs,
rhs,
},
span,
),
InfixKind::And => self.ast.push( InfixKind::And => self.ast.push(
Expr::Binary { Expr::Binary {
op: BinaryOp::And, op: BinaryOp::And,
@ -206,7 +270,7 @@ impl Parser {
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 => { TokenKind::Minus => {
let expr = self.parse_expr(11)?; let expr = self.parse_expr(17)?;
let span = token.span.join(self.ast.span(expr)); let span = token.span.join(self.ast.span(expr));
Ok(self.ast.push( Ok(self.ast.push(
Expr::Unary { Expr::Unary {
@ -216,6 +280,17 @@ impl Parser {
span, span,
)) ))
} }
TokenKind::Bang => {
let expr = self.parse_expr(17)?;
let span = token.span.join(self.ast.span(expr));
Ok(self.ast.push(
Expr::Unary {
op: UnaryOp::Not,
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,
@ -224,7 +299,7 @@ impl Parser {
TokenKind::Lte => CompareOp::Lte, TokenKind::Lte => CompareOp::Lte,
_ => unreachable!(), _ => unreachable!(),
}; };
let value = self.parse_expr(6)?; let value = self.parse_expr(12)?;
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))
} }
@ -449,10 +524,18 @@ 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::PipePipe => Some((InfixKind::LogicalOr, 7, 8)),
TokenKind::Minus => Some((InfixKind::Sub, 7, 8)), TokenKind::AmpAmp => Some((InfixKind::LogicalAnd, 9, 10)),
TokenKind::Star => Some((InfixKind::Mul, 9, 10)), TokenKind::EqualEqual => Some((InfixKind::Equal, 11, 12)),
TokenKind::Slash => Some((InfixKind::Div, 9, 10)), TokenKind::BangEqual => Some((InfixKind::NotEqual, 11, 12)),
TokenKind::Gt => Some((InfixKind::Greater, 11, 12)),
TokenKind::Gte => Some((InfixKind::GreaterEqual, 11, 12)),
TokenKind::Lt => Some((InfixKind::Less, 11, 12)),
TokenKind::Lte => Some((InfixKind::LessEqual, 11, 12)),
TokenKind::Plus => Some((InfixKind::Add, 13, 14)),
TokenKind::Minus => Some((InfixKind::Sub, 13, 14)),
TokenKind::Star => Some((InfixKind::Mul, 15, 16)),
TokenKind::Slash => Some((InfixKind::Div, 15, 16)),
_ => None, _ => None,
} }
} }
@ -549,6 +632,14 @@ enum InfixKind {
Sub, Sub,
Mul, Mul,
Div, Div,
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
LogicalAnd,
LogicalOr,
And, And,
Patch, Patch,
Default, Default,

View File

@ -0,0 +1,41 @@
# Logical and Comparison Expressions
Decodal supports boolean logic over concrete `Bool` values and comparison over concrete scalar values.
```dcdl
{
is_prod = env == "prod";
high_port = port > 9000;
enabled = is_prod && high_port;
disabled = !enabled;
}
```
## Logical operators
- `!expr` negates a concrete `Bool`.
- `lhs && rhs` returns boolean AND.
- `lhs || rhs` returns boolean OR.
`&&` and `||` short-circuit: the right-hand side is evaluated only when needed.
Logical operands must evaluate to concrete `Bool` values.
## Comparison operators
- `==`
- `!=`
- `<`
- `<=`
- `>`
- `>=`
`==` and `!=` compare concrete scalar values: `String`, `Bool`, `Int`, and `Float`.
`Int` and `Float` can be compared to each other numerically.
Ordering operators `<`, `<=`, `>`, and `>=` compare concrete numeric values only.
They are separate from prefix comparison constraints such as `> 443`.
```dcdl
port = Int & > 443 default 9443;
is_high = port > 9000;
```

View File

@ -7,12 +7,15 @@
優先順位は高い順に以下である。 優先順位は高い順に以下である。
1. 関数呼び出しとフィールド参照 1. 関数呼び出しとフィールド参照
2. unary `-` 2. unary `!` `-`
3. `*` `/` 3. `*` `/`
4. `+` `-` 4. `+` `-`
5. `&` 5. `==` `!=` `<` `<=` `>` `>=`
6. `//` 6. `&&`
7. `default` 7. `||`
8. `&`
9. `//`
10. `default`
同じ優先順位の二項演算子は左結合である。 同じ優先順位の二項演算子は左結合である。
`default` は右結合である。 `default` は右結合である。
@ -22,6 +25,15 @@
`+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。 `+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。
詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。 詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。
## Logical and comparison operators
`!` `&&` `||` は concrete `Bool` に対する論理演算である。
`&&``||` は短絡評価される。
`==` `!=` は concrete scalar value を比較する。
`<` `<=` `>` `>=` は concrete numeric value を比較する。
詳しくは [Logical and Comparison Expressions](./expression/logical-and-comparison.md) を参照する。
## `&`: 制約合成 ## `&`: 制約合成
`&` は値・制約・構造を合成する演算子である。 `&` は値・制約・構造を合成する演算子である。

View File

@ -115,12 +115,14 @@ rec
主要な演算子は以下である。 主要な演算子は以下である。
```text ```text
+ - * / 四則演算 + - * / 四則演算
& 制約合成 ! && || 論理演算
// patch 合成 == != < <= > >= 比較式
default fallback 指定 & 制約合成
=> 関数 // patch 合成
. フィールド参照 / ドットパス定義 default fallback 指定
=> 関数
. フィールド参照 / ドットパス定義
``` ```
演算子の優先順位は [合成演算子](./operators.md) で定義する。 演算子の優先順位は [合成演算子](./operators.md) で定義する。

View File

@ -102,3 +102,30 @@ Arithmetic
right: (literal (integer)))) right: (literal (integer))))
right: (unary_expression right: (unary_expression
operand: (literal (integer))))))) operand: (literal (integer)))))))
==================
Logical and comparison
==================
{
enabled = env == "prod" && replicas > 1;
disabled = !enabled || false;
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (binary_expression
left: (identifier)
right: (literal (string)))
right: (binary_expression
left: (identifier)
right: (literal (integer)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (unary_expression
operand: (identifier))
right: (literal (boolean))))))

View File

@ -1,12 +1,15 @@
const PREC = { const PREC = {
DEFAULT: 1, DEFAULT: 1,
PATCH: 2, PATCH: 2,
AND: 3, COMPOSE: 3,
ADD: 4, OR: 4,
MUL: 5, LOGICAL_AND: 5,
UNARY: 6, COMPARE: 6,
CALL: 7, ADD: 7,
PATH: 8, MUL: 8,
UNARY: 9,
CALL: 10,
PATH: 11,
}; };
function commaSep(rule) { function commaSep(rule) {
@ -163,17 +166,32 @@ module.exports = grammar({
field('field', $.identifier), field('field', $.identifier),
)), )),
comparison_constraint: $ => prec(6, seq( comparison_constraint: $ => prec.right(PREC.UNARY + 1, seq(
field('operator', choice('>', '>=', '<', '<=')), field('operator', choice('>', '>=', '<', '<=')),
field('value', $._expression), field('value', $._expression),
)), )),
unary_expression: $ => prec(PREC.UNARY, seq( unary_expression: $ => prec(PREC.UNARY, seq(
field('operator', '-'), field('operator', choice('-', '!')),
field('operand', $._expression), field('operand', $._expression),
)), )),
binary_expression: $ => choice( binary_expression: $ => choice(
prec.left(PREC.OR, seq(
field('left', $._expression),
field('operator', token(prec(2, '||'))),
field('right', $._expression),
)),
prec.left(PREC.LOGICAL_AND, seq(
field('left', $._expression),
field('operator', token(prec(2, '&&'))),
field('right', $._expression),
)),
prec.left(PREC.COMPARE, seq(
field('left', $._expression),
field('operator', choice('==', '!=', '>', '>=', '<', '<=')),
field('right', $._expression),
)),
prec.left(PREC.ADD, seq( prec.left(PREC.ADD, seq(
field('left', $._expression), field('left', $._expression),
field('operator', choice('+', '-')), field('operator', choice('+', '-')),
@ -184,7 +202,7 @@ module.exports = grammar({
field('operator', choice('*', '/')), field('operator', choice('*', '/')),
field('right', $._expression), field('right', $._expression),
)), )),
prec.left(PREC.AND, seq( prec.left(PREC.COMPOSE, seq(
field('left', $._expression), field('left', $._expression),
field('operator', '&'), field('operator', '&'),
field('right', $._expression), field('right', $._expression),

View File

@ -21,6 +21,11 @@
"-" "-"
"*" "*"
"/" "/"
"&&"
"||"
"!"
"=="
"!="
"=>" "=>"
"=" "="
">" ">"

View File

@ -716,7 +716,7 @@
}, },
"call_expression": { "call_expression": {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 7, "value": 10,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -786,7 +786,7 @@
}, },
"path_expression": { "path_expression": {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 8, "value": 11,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -814,8 +814,8 @@
} }
}, },
"comparison_constraint": { "comparison_constraint": {
"type": "PREC", "type": "PREC_RIGHT",
"value": 6, "value": 10,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -857,7 +857,7 @@
}, },
"unary_expression": { "unary_expression": {
"type": "PREC", "type": "PREC",
"value": 6, "value": 9,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -865,8 +865,17 @@
"type": "FIELD", "type": "FIELD",
"name": "operator", "name": "operator",
"content": { "content": {
"type": "STRING", "type": "CHOICE",
"value": "-" "members": [
{
"type": "STRING",
"value": "-"
},
{
"type": "STRING",
"value": "!"
}
]
} }
}, },
{ {
@ -886,6 +895,144 @@
{ {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 4, "value": 4,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "TOKEN",
"content": {
"type": "PREC",
"value": 2,
"content": {
"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": "TOKEN",
"content": {
"type": "PREC",
"value": 2,
"content": {
"type": "STRING",
"value": "&&"
}
}
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 6,
"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": "STRING",
"value": ">"
},
{
"type": "STRING",
"value": ">="
},
{
"type": "STRING",
"value": "<"
},
{
"type": "STRING",
"value": "<="
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 7,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -927,7 +1074,7 @@
}, },
{ {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 5, "value": 8,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [

View File

@ -152,10 +152,18 @@
"multiple": false, "multiple": false,
"required": true, "required": true,
"types": [ "types": [
{
"type": "!=",
"named": false
},
{ {
"type": "&", "type": "&",
"named": false "named": false
}, },
{
"type": "&&",
"named": false
},
{ {
"type": "*", "type": "*",
"named": false "named": false
@ -175,6 +183,30 @@
{ {
"type": "//", "type": "//",
"named": false "named": false
},
{
"type": "<",
"named": false
},
{
"type": "<=",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": ">",
"named": false
},
{
"type": ">=",
"named": false
},
{
"type": "||",
"named": false
} }
] ]
}, },
@ -1616,6 +1648,10 @@
"multiple": false, "multiple": false,
"required": true, "required": true,
"types": [ "types": [
{
"type": "!",
"named": false
},
{ {
"type": "-", "type": "-",
"named": false "named": false
@ -1624,10 +1660,22 @@
} }
} }
}, },
{
"type": "!",
"named": false
},
{
"type": "!=",
"named": false
},
{ {
"type": "&", "type": "&",
"named": false "named": false
}, },
{
"type": "&&",
"named": false
},
{ {
"type": "(", "type": "(",
"named": false "named": false
@ -1684,6 +1732,10 @@
"type": "=", "type": "=",
"named": false "named": false
}, },
{
"type": "==",
"named": false
},
{ {
"type": "=>", "type": "=>",
"named": false "named": false
@ -1764,6 +1816,10 @@
"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

11
examples/logical.dcdl Normal file
View File

@ -0,0 +1,11 @@
let
env = "prod";
replicas = 3;
in
{
is_prod = env == "prod";
scaled = replicas > 1;
enabled = env == "prod" && replicas > 1;
disabled = !(env == "prod" && replicas > 1);
safe = env != "dev" || replicas >= 1;
}

View File

@ -51,6 +51,7 @@ export const nav = [
{ 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: 'Arithmetic', slug: 'language/expression/arithmetic' },
{ title: 'Logical and Comparison', slug: 'language/expression/logical-and-comparison' },
{ title: 'String Interpolation', slug: 'language/expression/string-interpolation' }, { title: 'String Interpolation', slug: 'language/expression/string-interpolation' },
], ],
}, },

View File

@ -195,7 +195,7 @@ function isOperatorStart(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[index] === source[start]) 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

@ -10,6 +10,7 @@ in
schema.Service & { schema.Service & {
name = "api"; name = "api";
port = 9000 + 443; port = 9000 + 443;
feature.enable = 9000 + 443 > 9000 && true;
} }
`, `,
'schemas/service.dcdl': `Service = { 'schemas/service.dcdl': `Service = {