Add array concat operator

This commit is contained in:
Keisuke Hirata 2026-06-19 01:01:04 +09:00
parent 6da0ec4c77
commit 01ad6dca52
No known key found for this signature in database
17 changed files with 6365 additions and 5138 deletions

View File

@ -133,6 +133,7 @@ pub enum BinaryOp {
Sub, Sub,
Mul, Mul,
Div, Div,
Concat,
Equal, Equal,
NotEqual, NotEqual,
Greater, Greater,

View File

@ -410,6 +410,7 @@ impl<L: SourceLoader> Engine<L> {
BinaryOp::Div => { BinaryOp::Div => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Div, span) arithmetic(lhs_value, rhs_value, ArithmeticOp::Div, span)
} }
BinaryOp::Concat => concat_arrays(lhs_value, rhs_value, span),
BinaryOp::Equal => { BinaryOp::Equal => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Equal, span) compare_expr(lhs_value, rhs_value, CompareExprOp::Equal, span)
} }
@ -1208,6 +1209,23 @@ fn comparison_type_error(span: Span) -> Diagnostic {
) )
} }
fn concat_arrays(lhs: RuntimeValue, rhs: RuntimeValue, span: Span) -> Result<RuntimeValue> {
match (lhs, rhs) {
(
RuntimeValue::Concrete(ConcreteValue::Array(mut lhs)),
RuntimeValue::Concrete(ConcreteValue::Array(rhs)),
) => {
lhs.extend(rhs);
Ok(RuntimeValue::Concrete(ConcreteValue::Array(lhs)))
}
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"'++' expects array values",
)),
}
}
fn arithmetic( fn arithmetic(
lhs: RuntimeValue, lhs: RuntimeValue,
rhs: RuntimeValue, rhs: RuntimeValue,
@ -1395,6 +1413,32 @@ mod tests {
assert!(engine.eval_root().is_err()); assert!(engine.eval_root().is_err());
} }
#[test]
fn evaluates_array_concat() {
let data = eval_data(
r#"
[1, 2] ++ [3, 4]
"#,
);
assert_eq!(
data,
Data::Array(vec![Data::Int(1), Data::Int(2), Data::Int(3), Data::Int(4)])
);
}
#[test]
fn array_concat_has_lower_precedence_than_arithmetic() {
let data = eval_data("[1 + 1] ++ [2 * 2]");
assert_eq!(data, Data::Array(vec![Data::Int(2), Data::Int(4)]));
}
#[test]
fn rejects_invalid_array_concat_operands() {
let parsed = parse_source("[1] ++ 2").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test] #[test]
fn evaluates_logical_and_comparison_expressions() { fn evaluates_logical_and_comparison_expressions() {
let data = eval_data( let data = eval_data(

View File

@ -42,6 +42,7 @@ pub enum TokenKind {
AmpAmp, AmpAmp,
PipePipe, PipePipe,
Plus, Plus,
PlusPlus,
Minus, Minus,
Star, Star,
Slash, Slash,
@ -166,7 +167,11 @@ impl<'a> Lexer<'a> {
} }
b'+' => { b'+' => {
self.pos += 1; self.pos += 1;
TokenKind::Plus if self.consume(b'+') {
TokenKind::PlusPlus
} else {
TokenKind::Plus
}
} }
b'-' => { b'-' => {
self.pos += 1; self.pos += 1;

View File

@ -147,6 +147,14 @@ impl Parser {
}, },
span, span,
), ),
InfixKind::Concat => self.ast.push(
Expr::Binary {
op: BinaryOp::Concat,
lhs,
rhs,
},
span,
),
InfixKind::Equal => self.ast.push( InfixKind::Equal => self.ast.push(
Expr::Binary { Expr::Binary {
op: BinaryOp::Equal, op: BinaryOp::Equal,
@ -532,6 +540,7 @@ impl Parser {
TokenKind::Gte => Some((InfixKind::GreaterEqual, 11, 12)), TokenKind::Gte => Some((InfixKind::GreaterEqual, 11, 12)),
TokenKind::Lt => Some((InfixKind::Less, 11, 12)), TokenKind::Lt => Some((InfixKind::Less, 11, 12)),
TokenKind::Lte => Some((InfixKind::LessEqual, 11, 12)), TokenKind::Lte => Some((InfixKind::LessEqual, 11, 12)),
TokenKind::PlusPlus => Some((InfixKind::Concat, 12, 13)),
TokenKind::Plus => Some((InfixKind::Add, 13, 14)), TokenKind::Plus => Some((InfixKind::Add, 13, 14)),
TokenKind::Minus => Some((InfixKind::Sub, 13, 14)), TokenKind::Minus => Some((InfixKind::Sub, 13, 14)),
TokenKind::Star => Some((InfixKind::Mul, 15, 16)), TokenKind::Star => Some((InfixKind::Mul, 15, 16)),
@ -632,6 +641,7 @@ enum InfixKind {
Sub, Sub,
Mul, Mul,
Div, Div,
Concat,
Equal, Equal,
NotEqual, NotEqual,
Greater, Greater,

View File

@ -7,9 +7,21 @@ array expression は、順序付きの値の列を表す。
["a", "b", "c"] ["a", "b", "c"]
``` ```
## 未確定事項 ## Array concat
- 配列要素の制約表現。 `++` は concrete array 同士を連結する。
- 異種配列を許可するか。
- `//` による patch を右辺置換だけにするか。 ```dcdl
- append / prepend / remove などの操作を提供するか。 base = ["read", "write"];
extra = ["admin"];
roles = base ++ extra;
```
`roles` は以下と同じ値になる。
```dcdl
["read", "write", "admin"]
```
`++` は配列要素を変換しない。
左辺の要素の後に右辺の要素が並ぶ。

View File

@ -14,6 +14,7 @@
| `/` | `lhs / rhs` | arithmetic | concrete `Int` / `Float` | `Float` quotient | | `/` | `lhs / rhs` | arithmetic | concrete `Int` / `Float` | `Float` quotient |
| `+` | `lhs + rhs` | arithmetic | concrete `Int` / `Float` | numeric sum | | `+` | `lhs + rhs` | arithmetic | concrete `Int` / `Float` | numeric sum |
| `-` | `lhs - rhs` | arithmetic | concrete `Int` / `Float` | numeric difference | | `-` | `lhs - rhs` | arithmetic | concrete `Int` / `Float` | numeric difference |
| `++` | `lhs ++ rhs` | array concat | concrete arrays | concatenated array |
| `==` | `lhs == rhs` | equality | concrete scalar | concrete `Bool` | | `==` | `lhs == rhs` | equality | concrete scalar | concrete `Bool` |
| `!=` | `lhs != rhs` | equality | concrete scalar | concrete `Bool` | | `!=` | `lhs != rhs` | equality | concrete scalar | concrete `Bool` |
| `<` | `lhs < rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` | | `<` | `lhs < rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
@ -40,12 +41,13 @@
2. unary `!` `-` 2. unary `!` `-`
3. `*` `/` 3. `*` `/`
4. `+` `-` 4. `+` `-`
5. `==` `!=` `<` `<=` `>` `>=` 5. `++`
6. `&&` 6. `==` `!=` `<` `<=` `>` `>=`
7. `||` 7. `&&`
8. `&` 8. `||`
9. `//` 9. `&`
10. `default` 10. `//`
11. `default`
同じ優先順位の二項演算子は左結合である。 同じ優先順位の二項演算子は左結合である。
`default` は右結合である。 `default` は右結合である。
@ -55,6 +57,15 @@
`+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。 `+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。
詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。 詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。
## Array concat operator
`++` は concrete array 同士を連結する演算子である。
要素は変換されず、左辺の要素の後に右辺の要素が並ぶ。
```dcdl
["read", "write"] ++ ["admin"]
```
## Logical and comparison operators ## Logical and comparison operators
`!` `&&` `||` は concrete `Bool` に対する論理演算である。 `!` `&&` `||` は concrete `Bool` に対する論理演算である。

View File

@ -116,6 +116,7 @@ rec
```text ```text
+ - * / 四則演算 + - * / 四則演算
++ 配列結合
! && || 論理演算 ! && || 論理演算
== != < <= > >= 比較式 == != < <= > >= 比較式
& 制約合成 & 制約合成

View File

@ -129,3 +129,32 @@ Logical and comparison
left: (unary_expression left: (unary_expression
operand: (identifier)) operand: (identifier))
right: (literal (boolean)))))) right: (literal (boolean))))))
==================
Array concat
==================
{
roles = ["read"] ++ ["write", "admin"];
ports = [8000 + 80] ++ [9443];
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (array
(literal (string)))
right: (array
(literal (string))
(literal (string)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (array
(binary_expression
left: (literal (integer))
right: (literal (integer))))
right: (array
(literal (integer)))))))

View File

@ -5,11 +5,12 @@ const PREC = {
OR: 4, OR: 4,
LOGICAL_AND: 5, LOGICAL_AND: 5,
COMPARE: 6, COMPARE: 6,
ADD: 7, CONCAT: 7,
MUL: 8, ADD: 8,
UNARY: 9, MUL: 9,
CALL: 10, UNARY: 10,
PATH: 11, CALL: 11,
PATH: 12,
}; };
function commaSep(rule) { function commaSep(rule) {
@ -192,6 +193,11 @@ module.exports = grammar({
field('operator', choice('==', '!=', '>', '>=', '<', '<=')), field('operator', choice('==', '!=', '>', '>=', '<', '<=')),
field('right', $._expression), field('right', $._expression),
)), )),
prec.left(PREC.CONCAT, seq(
field('left', $._expression),
field('operator', token(prec(2, '++'))),
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('+', '-')),

View File

@ -21,6 +21,7 @@
"-" "-"
"*" "*"
"/" "/"
"++"
"&&" "&&"
"||" "||"
"!" "!"

View File

@ -716,7 +716,7 @@
}, },
"call_expression": { "call_expression": {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 10, "value": 11,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -786,7 +786,7 @@
}, },
"path_expression": { "path_expression": {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 11, "value": 12,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -815,7 +815,7 @@
}, },
"comparison_constraint": { "comparison_constraint": {
"type": "PREC_RIGHT", "type": "PREC_RIGHT",
"value": 10, "value": 11,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -857,7 +857,7 @@
}, },
"unary_expression": { "unary_expression": {
"type": "PREC", "type": "PREC",
"value": 9, "value": 10,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -1033,6 +1033,46 @@
{ {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 7, "value": 7,
"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": 8,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@ -1074,7 +1114,7 @@
}, },
{ {
"type": "PREC_LEFT", "type": "PREC_LEFT",
"value": 8, "value": 9,
"content": { "content": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [

View File

@ -172,6 +172,10 @@
"type": "+", "type": "+",
"named": false "named": false
}, },
{
"type": "++",
"named": false
},
{ {
"type": "-", "type": "-",
"named": false "named": false
@ -1692,6 +1696,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

View File

@ -0,0 +1,6 @@
{
base_roles = ["read", "write"];
extra_roles = ["admin"];
roles = ["read", "write"] ++ ["admin"];
ports = [8000 + 80] ++ [9000 + 443];
}

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[start] === '|' || source[start] === '/') && source[index] === source[start]) return index + 1; if ('&|/+'.includes(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;
tags = ["web"] ++ ["prod"];
feature.enable = 9000 + 443 > 9000 && true; feature.enable = 9000 + 443 > 9000 && true;
} }
`, `,