Skip to content

Commit 34dd02d

Browse files
reidspencerclaude
andcommitted
Add when/else/end syntax with identifier conditions
Extends WhenStatement to support: - Identifier conditions (from let bindings) in addition to literal strings - Negated identifiers using ! prefix (e.g., when !valid then) - Optional else block for handling false conditions Parser changes: - Added 'else' to Punctuation.scala - StatementParser now handles WhenCondition union type - Supports when <cond> then <stmts> [else <stmts>] end BAST serialization: - BASTWriter/Reader updated for new WhenStatement structure - Handles union type (LiteralString | Identifier) for condition - Supports negation flag and optional elseStatements Documentation: - Updated conditional.md with comprehensive examples - Removed Reply and Stop from statement.md (not valid RIDDL) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3662835 commit 34dd02d

11 files changed

Lines changed: 132 additions & 28 deletions

File tree

doc/src/main/hugo/content/concepts/conditional.md

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,36 +9,79 @@ RIDDL provides two conditional constructs: `when` for simple conditions and
99

1010
## When Statement
1111

12-
The `when` statement executes a block of statements when a condition is true:
12+
The `when` statement executes a block of statements when a condition is true.
13+
Conditions can be literal strings or identifier references (from `let` bindings).
14+
An optional `else` block handles the false case:
1315

1416
```riddl
1517
handler MyHandler is {
1618
on command DoSomething {
1719
let authorized = "user has permission"
1820
when authorized then
1921
send event ActionCompleted to outlet Events
20-
end
21-
when !authorized then
22+
else
2223
error "User not authorized"
2324
end
2425
}
2526
}
2627
```
2728

28-
### Using Let with When
29+
### Condition Types
30+
31+
The `when` statement accepts three forms of conditions:
32+
33+
1. **Literal string**: A quoted condition description
34+
```riddl
35+
when "user is authenticated" then
36+
// actions
37+
end
38+
```
39+
40+
2. **Identifier reference**: Reference a condition bound with `let`
41+
```riddl
42+
let valid = "input passes validation"
43+
when valid then
44+
// actions
45+
end
46+
```
47+
48+
3. **Negated identifier**: Use `!` to negate an identifier
49+
```riddl
50+
when !valid then
51+
error "Validation failed"
52+
end
53+
```
54+
55+
### Using Else Blocks
56+
57+
The `else` block is optional and executes when the condition is false:
58+
59+
```riddl
60+
let condition = "some boolean expression"
61+
when condition then
62+
// executed when true
63+
else
64+
// executed when false
65+
end
66+
```
67+
68+
### Alternative: Multiple When Statements
2969

30-
The `let` statement binds a condition to a name for reuse:
70+
You can also use separate `when` statements with negation:
3171

3272
```riddl
3373
let condition = "some boolean expression"
3474
when condition then
3575
// executed when true
3676
end
3777
when !condition then
38-
// executed when false (note the ! negation)
78+
// executed when false
3979
end
4080
```
4181

82+
Both approaches are valid. Use `else` when the two blocks are closely related,
83+
and separate `when` statements when they represent distinct scenarios.
84+
4285
## Match Statement
4386

4487
The `match` statement handles multiple cases with pattern matching:

doc/src/main/hugo/content/concepts/statement.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,11 @@ kinds of statements, described in the table below.
1818
| Match | `match "scenario" { case "cond1" { } default { } }` | Select from multiple conditions/cases |
1919
| Morph | `morph entity E to state S with record R` | Morph the state of an entity to a new state |
2020
| Prompt | `prompt "description of action"` | A textually described action to be implemented |
21-
| Reply | `reply result R` or `reply record R` | Provide the reply to a query |
2221
| Return | `return value` | Return a value from a function |
2322
| Send | `send event E to outlet O` | Send a message to an outlet (asynchronous) |
2423
| Set | `set field S.f to "value"` | Set a field value |
25-
| Stop | `stop` | Stop processing further statements |
2624
| Tell | `tell command C to entity E` | Send a message to an entity directly |
27-
| When | `when condition then ... end` | Execute statements when a condition is true |
25+
| When | `when cond then ... [else ...] end` | Execute statements conditionally with optional else |
2826

2927
## Level of Detail
3028

language/shared/src/main/scala/com/ossuminc/riddl/language/AST.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2453,23 +2453,39 @@ object AST:
24532453
def format: String = s"tell ${msg.format} to ${processorRef.format}"
24542454
}
24552455

2456-
/** A guard clause statement for conditional early exit with validation
2456+
/** A conditional statement for branching logic
24572457
*
24582458
* @param loc
24592459
* The location of the statement in the model
24602460
* @param condition
2461-
* The boolean expression to evaluate (as a string, evaluated at runtime)
2461+
* The boolean expression to evaluate - either a literal string description
2462+
* or an identifier referencing a let binding
24622463
* @param thenStatements
24632464
* The statements to execute if the condition is true
2465+
* @param elseStatements
2466+
* The statements to execute if the condition is false (optional)
24642467
*/
24652468
@JSExportTopLevel("WhenStatement")
24662469
case class WhenStatement(
24672470
loc: At,
2468-
condition: LiteralString,
2469-
thenStatements: Contents[Statements]
2471+
condition: LiteralString | Identifier,
2472+
thenStatements: Contents[Statements],
2473+
elseStatements: Contents[Statements] = Contents.empty[Statements](0),
2474+
negated: Boolean = false
24702475
) extends Statement {
24712476
override def kind: String = "When Statement"
2472-
def format: String = s"when ${condition.format} then\n${thenStatements.toSeq.map(_.format).mkString("\n ")}\n end"
2477+
def format: String = {
2478+
val condStr = condition match {
2479+
case ls: LiteralString => ls.format
2480+
case id: Identifier => if negated then s"!${id.format}" else id.format
2481+
}
2482+
val thenStr = if thenStatements.isEmpty then "" else thenStatements.toSeq.map(_.format).mkString("\n ")
2483+
if elseStatements.isEmpty then s"when $condStr then\n$thenStr\n end"
2484+
else {
2485+
val elseStr = if elseStatements.isEmpty then "" else elseStatements.toSeq.map(_.format).mkString("\n ")
2486+
s"when $condStr then\n$thenStr\nelse\n$elseStr\n end"
2487+
}
2488+
}
24732489
}
24742490

24752491
/** A case clause within a match statement */

language/shared/src/main/scala/com/ossuminc/riddl/language/Finder.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ case class Finder[CV <: RiddlValue](root: Container[CV]) {
7171
child match
7272
case c: Container[?] =>
7373
c.contents.foldLeft(list) { case (next, child) => consider(next, child) }
74-
case WhenStatement(_, _, thenStatements) =>
75-
thenStatements.foldLeft(list) { case (next, child) => consider(next, child) }
74+
case WhenStatement(_, _, thenStatements, elseStatements, _) =>
75+
val r1 = thenStatements.foldLeft(list) { case (next, child) => consider(next, child) }
76+
elseStatements.foldLeft(r1) { case (next, child) => consider(next, child) }
7677
case MatchStatement(_, _, cases, default) =>
7778
val r1 = cases.foldLeft(list) { case (next, mc) =>
7879
mc.statements.foldLeft(next) { case (next2, child) => consider(next2, child) }

language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTReader.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -718,9 +718,16 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) {
718718
TellStatement(loc, msg, processorRef)
719719

720720
case 10 => // When
721-
val condition = readLiteralString()
721+
val conditionType = reader.readU8()
722+
val condition: LiteralString | Identifier = conditionType match {
723+
case 0 => readLiteralString()
724+
case 1 => readIdentifierInline()
725+
case _ => throw new RuntimeException(s"Invalid when condition type: $conditionType")
726+
}
727+
val negated = reader.readU8() != 0
722728
val thenStatements = readContentsDeferred[Statements]()
723-
WhenStatement(loc, condition, thenStatements)
729+
val elseStatements = readContentsDeferred[Statements]()
730+
WhenStatement(loc, condition, thenStatements, elseStatements, negated)
724731

725732
case 11 => // Match
726733
val expression = readLiteralString()

language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTWriter.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -874,8 +874,18 @@ class BASTWriter(val writer: ByteBufferWriter, val stringTable: StringTable) {
874874
writer.writeU8(NODE_STATEMENT)
875875
writer.writeU8(10) // When statement
876876
writeLocation(s.loc)
877-
writeLiteralString(s.condition)
878-
// NOTE: thenStatements count/items are written by the Pass's traverse() override
877+
// Write condition with type flag (0=LiteralString, 1=Identifier)
878+
s.condition match {
879+
case ls: LiteralString =>
880+
writer.writeU8(0)
881+
writeLiteralString(ls)
882+
case id: Identifier =>
883+
writer.writeU8(1)
884+
writeIdentifierInline(id)
885+
}
886+
// Write negated flag (0=not negated, 1=negated)
887+
writer.writeU8(if s.negated then 1 else 0)
888+
// NOTE: thenStatements and elseStatements counts/items are written by the Pass's traverse() override
879889
}
880890

881891
def writeMatchStatement(s: MatchStatement): Unit = {

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/Punctuation.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ object Punctuation {
1717
final val curlyClose = "}"
1818
final val dot = "."
1919
final val equalsSign = "="
20+
final val exclamation = "!"
2021
final val plus = "+"
2122
final val question = "?"
2223
final val quote = "\""

language/shared/src/main/scala/com/ossuminc/riddl/language/parsing/StatementParser.scala

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,23 @@ private[parsing] trait StatementParser {
7171
)./.map { case (start, eRef, hRef, end) => BecomeStatement(at(start, end), eRef, hRef) }
7272
}
7373

74+
private def whenCondition[u: P]: P[(LiteralString | Identifier, Boolean)] = {
75+
P(
76+
literalString.map(ls => (ls, false)) |
77+
(Punctuation.exclamation ~ identifier).map { case id => (id, true) } |
78+
identifier.map(id => (id, false))
79+
)
80+
}
81+
7482
private def whenStatement[u: P](set: StatementsSet): P[WhenStatement] = {
7583
P(
76-
Index ~ Keywords.when ~/ literalString ~ Keywords.`then` ~/ pseudoCodeBlock(set) ~/ Keywords.end_ ~/ Index
77-
)./.map { case (start, cond, statements, end) =>
78-
WhenStatement(at(start, end), cond, statements.toContents)
84+
Index ~ Keywords.when ~/ whenCondition ~ Keywords.`then` ~/
85+
pseudoCodeBlock(set) ~/
86+
(Keywords.else_ ~/ pseudoCodeBlock(set)).? ~/
87+
Keywords.end_ ~/ Index
88+
)./.map { case (start, (cond, negated), thenStmts, elseStmtsOpt, end) =>
89+
val elseStmts = elseStmtsOpt.getOrElse(Seq.empty[Statements])
90+
WhenStatement(at(start, end), cond, thenStmts.toContents, elseStmts.toContents, negated)
7991
}
8092
}
8193

passes/shared/src/main/scala/com/ossuminc/riddl/passes/BASTWriterPass.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ case class BASTWriterPass(input: PassInput, outputs: PassesOutput)(using pc: Pla
8787
// Phase 7: Only write metadata if non-empty (flag was set)
8888
if ss.metadata.nonEmpty then bastWriter.writeMetadataCount(ss.metadata)
8989

90-
// WhenStatement has a thenStatements Contents field that must be traversed
90+
// WhenStatement has thenStatements and elseStatements Contents fields that must be traversed
9191
case ws: WhenStatement =>
9292
process(ws, parents)
9393
bastWriter.writeContents(ws.thenStatements)
9494
ws.thenStatements.toSeq.foreach { value => traverse(value, parents) }
95+
bastWriter.writeContents(ws.elseStatements)
96+
ws.elseStatements.toSeq.foreach { value => traverse(value, parents) }
9597
// WhenStatement is a Statement, no metadata
9698

9799
// MatchStatement has cases and default Contents that must be traversed

passes/shared/src/main/scala/com/ossuminc/riddl/passes/prettify/RiddlFileEmitter.scala

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,17 @@ case class RiddlFileEmitter(url: URL) extends FileBuilder {
297297

298298
def emitStatement(statement: Statements): Unit =
299299
statement match
300-
case WhenStatement(_, cond, thenStatements) =>
301-
addIndent(s"when ${cond.format} then").nl.incr
300+
case WhenStatement(_, cond, thenStatements, elseStatements, negated) =>
301+
val condStr = cond match {
302+
case ls: LiteralString => ls.format
303+
case id: Identifier => if negated then s"!${id.format}" else id.format
304+
}
305+
addIndent(s"when $condStr then").nl.incr
302306
if thenStatements.isEmpty then addLine("???") else thenStatements.toSeq.foreach(emitStatement)
307+
if elseStatements.nonEmpty then
308+
decr.addLine("else").incr
309+
elseStatements.toSeq.foreach(emitStatement)
310+
end if
303311
decr.addLine("end")
304312
case MatchStatement(_, expr, cases, default) =>
305313
addIndent(s"match ${expr.format} {").nl.incr

0 commit comments

Comments
 (0)