Skip to content

Commit 7ae4251

Browse files
reid-spencerclaude
andcommitted
Change State from Leaf to Branch, allowing handlers in State scope
State definitions inside entities can now contain handlers, enabling state-machine modeling where behavior changes when a new state is activated. The traditional bare syntax (state X of Y) still works. New syntax: state X of Y is { handler H is { ... } } Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent acb94b2 commit 7ae4251

13 files changed

Lines changed: 107 additions & 45 deletions

File tree

.claude/skills/ship/SKILL.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -126,30 +126,6 @@ not provided:
126126
15. Report a summary: tag, commit SHA, release URL, and any
127127
CI workflows triggered.
128128
129-
16. **Drop upgrade tasks in dependent projects.** For each
130-
consumer of riddl, create a task file in its `task/`
131-
directory describing the version bump needed. The file
132-
should be named `upgrade-riddl-<VERSION>.md` and contain:
133-
- What changed (link to the GitHub release)
134-
- The new version to depend on
135-
- Which files to update (e.g., `project/Dependencies.scala`,
136-
`build.sbt`, or `package.json`)
137-
138-
Consumer projects:
139-
```
140-
../synapify/task/upgrade-riddl-<VERSION>.md
141-
../riddl-idea-plugin/task/upgrade-riddl-<VERSION>.md
142-
../riddlsim/task/upgrade-riddl-<VERSION>.md
143-
../ossum.tech/task/upgrade-riddl-<VERSION>.md
144-
../riddl-mcp-server/task/upgrade-riddl-<VERSION>.md
145-
../riddl-models/task/upgrade-riddl-<VERSION>.md
146-
../ossum.ai/task/upgrade-riddl-<VERSION>.md
147-
../riddl-gen/task/upgrade-riddl-<VERSION>.md
148-
```
149-
150-
Also update the `riddl dep` column in
151-
`../CLAUDE.md` (ossuminc-level) to reflect `<VERSION>`.
152-
153129
## If Something Fails
154130
155131
- If tests fail in step 9: delete the local tag
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Test: State definitions with handlers (State as Branch)
2+
domain StateHandlerTest is {
3+
type SomeType is String
4+
command Activate()
5+
command Deactivate()
6+
event Activated()
7+
event Deactivated()
8+
9+
context TestContext is {
10+
entity StateMachine is {
11+
record ActiveFields(status: Boolean)
12+
record InactiveFields(reason: String)
13+
14+
// State with handlers - the new syntax
15+
state ActiveState of StateMachine.ActiveFields is {
16+
handler onActive is {
17+
on command Deactivate {
18+
send event Deactivated to outlet TestContext.Sink.Out
19+
}
20+
}
21+
}
22+
23+
// State with handlers using multiple handlers
24+
state InactiveState of StateMachine.InactiveFields is {
25+
handler onInactive is {
26+
on command Activate {
27+
send event Activated to outlet TestContext.Sink.Out
28+
}
29+
}
30+
}
31+
32+
// State without handlers - the traditional syntax
33+
state PlainState of StateMachine.ActiveFields
34+
35+
handler default is {
36+
on command Activate { ??? }
37+
}
38+
}
39+
40+
source Sink is {
41+
outlet Out is type SomeType
42+
} with { briefly "A source" }
43+
}
44+
} with {
45+
briefly "Test domain for state-with-handlers feature"
46+
}

language/shared/src/main/resources/riddl/grammar/ebnf-grammar.ebnf

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ entity = "entity" identifier is "{" entity_body "}" [with_metadata] ;
6464
entity_body = entity_definitions | "???" ;
6565
entity_definitions = {entity_content}+ ;
6666
entity_content = processor_definition_contents | state | entity_include ;
67-
state = "state" identifier ("of" | is) type_ref [with_metadata] ;
67+
state = "state" identifier ("of" | is) type_ref [state_body] [with_metadata] ;
68+
state_body = is "{" (state_content+ | "???") "}" ;
69+
state_content = handler | comment ;
6870
entity_include = "include" literal_string ;
6971
7072
(* Processor Definition Contents *)

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,9 @@ object AST:
730730
/** Type of definitions that occur in an [[Entity]] with [[Include]] */
731731
type EntityContents = OccursInEntity | Include[OccursInEntity]
732732

733+
/** Type of definitions that occur in a [[State]] */
734+
type StateContents = Handler | Comment
735+
733736
/** Type of definitions that occur in a [[Handler]] */
734737
type HandlerContents = OnClause | Comment
735738

@@ -2806,8 +2809,10 @@ object AST:
28062809
loc: At,
28072810
id: Identifier,
28082811
typ: TypeRef,
2812+
contents: Contents[StateContents] = Contents.empty[StateContents](),
28092813
metadata: Contents[MetaData] = Contents.empty[MetaData]()
2810-
) extends Leaf:
2814+
) extends Branch[StateContents]
2815+
with WithHandlers[StateContents]:
28112816
def format: String = Keyword.state + " " + id.format
28122817
end State
28132818

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -853,8 +853,9 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) {
853853
val loc = readLocation()
854854
val id = readIdentifierInline() // Inline - no tag
855855
val typ = readTypeRefInline() // Inline - position known
856+
val contents = readContentsDeferred[StateContents]()
856857
val metadata = readMetadataDeferred()
857-
State(loc, id, typ, metadata)
858+
State(loc, id, typ, contents, metadata)
858859
}
859860

860861
private def readInvariantNode(): Invariant = {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@ class BASTWriter(val writer: ByteBufferWriter, val stringTable: StringTable) {
569569
writeLocation(s.loc)
570570
writeIdentifierInline(s.id) // Inline - no tag needed
571571
writeTypeRefInline(s.typ) // Inline - position known
572+
writeContents(s.contents)
572573
}
573574

574575
def writeInvariant(i: Invariant): Unit = {

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,23 @@ import fastparse.MultiLineWhitespace.*
1515
private[parsing] trait EntityParser {
1616
this: ProcessorParser & StreamingParser =>
1717

18+
private def stateContent[u: P]: P[StateContents] =
19+
P(handler(StatementsSet.EntityStatements) | comment).asInstanceOf[P[StateContents]]
20+
21+
private def stateContents[u: P]: P[Seq[StateContents]] =
22+
stateContent.rep(1)
23+
24+
private def stateBody[u: P]: P[Seq[StateContents]] =
25+
P(is ~ open ~ (undefined(Seq.empty[StateContents]) | stateContents) ~ close)
26+
1827
def state[u: P]: P[State] = {
1928
P(
20-
Index ~ Keywords.state ~ identifier ~/ (of | is) ~ typeRef ~/ withMetaData ~ Index
21-
)./.map { case (start, id, typRef, descriptives, end) =>
22-
State(at(start,end), id, typRef, descriptives.toContents)
29+
Index ~ Keywords.state ~ identifier ~/ (of | is) ~ typeRef ~/
30+
stateBody.? ~ withMetaData ~ Index
31+
)./.map { case (start, id, typRef, body, descriptives, end) =>
32+
State(at(start, end), id, typRef,
33+
body.getOrElse(Seq.empty).toContents,
34+
descriptives.toContents)
2335
}
2436
}
2537

passes/jvm-native/src/test/scala/com/ossuminc/riddl/passes/PassTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ class PassTest extends AbstractTestingBasisWithTestData {
159159
Pass.runPass[PassOutput](input, outputs, hp)
160160
val (opens, closes, leaves, values) = hp.processForTest(result.root, ParentStack.empty)
161161
opens.must(be(closes))
162-
opens.must(be(55))
162+
opens.must(be(57))
163163
values.must(be(25))
164-
leaves.must(be(19))
164+
leaves.must(be(17))
165165
}
166166
Await.result(future, 10.seconds)
167167
}

passes/jvm-native/src/test/scala/com/ossuminc/riddl/passes/StatsPassTest.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ class StatsPassTest extends AbstractValidatingTest {
6262
statsOutput.maximum_depth > 0 mustBe true
6363
statsOutput.categories mustNot be(empty)
6464
statsOutput.categories.toSeq.foreach { pair => println(pair._1 + s" => ${pair._2.count}") }
65-
statsOutput.categories.size must be(22)
65+
statsOutput.categories.size must be(23)
6666
val ksAll: KindStats = statsOutput.categories("All")
67-
ksAll.count must be(21)
68-
ksAll.numEmpty must be(21)
67+
ksAll.count must be(22)
68+
ksAll.numEmpty must be(23)
6969
ksAll.numStatements must be(7)
7070
succeed
7171
}

passes/jvm-native/src/test/scala/com/ossuminc/riddl/passes/VisitingPassTest.scala

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class VisitingPassTest extends ParsingTest {
4141
// |closes=${visitor.closes}
4242
// |""".stripMargin)
4343
visitor.depth must be(0)
44-
visitor.leaves must be(19)
44+
visitor.leaves must be(17)
4545
visitor.values must be(24)
4646
visitor.opens must be(visitor.closes)
4747
end match
@@ -128,7 +128,8 @@ class TestVisitor extends PassVisitor:
128128
def doConnector(connector: Connector): Unit = leaf(connector)
129129
def doUser(user: User): Unit = leaf(user)
130130
def doSchema(schema: Schema): Unit = leaf(schema)
131-
def doState(state: State): Unit = leaf(state)
131+
def openState(state: State, parents: Parents): Unit = incr(state)
132+
def closeState(state: State, parents: Parents): Unit = decr(state)
132133
def doRelationship(relationship: Relationship): Unit = leaf(relationship)
133134
def doContainedGroup(containedGroup: ContainedGroup): Unit = leaf(containedGroup)
134135

0 commit comments

Comments
 (0)