Skip to content

Commit a296806

Browse files
reid-spencerclaude
andcommitted
Add optional type annotation to let statement and improve validation
Language changes: - let statement now accepts optional type annotation: let x: Type = "value" - EBNF grammar, parser, AST, BAST reader/writer, prettifier all updated - BAST FORMAT_REVISION bumped to 3 Validation improvements: - Promote cross-context reference warning from StyleWarning to Warning with guidance to use Adaptors or Streamlet pipelines instead - Suppress cross-context reference warning inside Adaptors (their purpose is cross-context communication) - Suppress "commands should emit events" warning for Adaptors and external Contexts (one-way communication is expected) - Fix isAdaptor check to use parents.exists(_.isInstanceOf[Adaptor]) - Allow 'external' option on Context in addition to Domain - Fix adaptor-direction check file for removed adaptor command warning Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 536d836 commit a296806

14 files changed

Lines changed: 106 additions & 30 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ tell_statement = "tell" message_ref "to" processor_ref ;
204204
205205
(* Variable operations *)
206206
the_set_statement = "set" field_ref "to" literal_string ;
207-
let_statement = "let" identifier "=" literal_string ;
207+
let_statement = "let" identifier [":" type_ref] "=" literal_string ;
208208
209209
(* General statements *)
210210
prompt_statement = ("prompt" | "do") literal_string ;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2530,10 +2530,13 @@ object AST:
25302530
case class LetStatement(
25312531
loc: At,
25322532
identifier: Identifier,
2533+
typeRef: Option[TypeRef],
25332534
expression: LiteralString
25342535
) extends Statement {
25352536
override def kind: String = "Let Statement"
2536-
def format: String = s"let ${identifier.format} = ${expression.format}"
2537+
def format: String =
2538+
val typeClause = typeRef.map(t => s": ${t.format}").getOrElse("")
2539+
s"let ${identifier.format}$typeClause = ${expression.format}"
25372540
}
25382541

25392542
/** A code statement that contains arbitrary code in a specified language

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,8 +836,13 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) {
836836

837837
case 12 => // Let
838838
val identifier = readIdentifier()
839+
val hasTypeRef = reader.readU8() != 0
840+
val optTypeRef = if hasTypeRef then
841+
val pid = readPathIdentifierNode()
842+
Some(TypeRef(loc, "type", pid))
843+
else None
839844
val expression = readLiteralString()
840-
LetStatement(loc, identifier, expression)
845+
LetStatement(loc, identifier, optTypeRef, expression)
841846

842847
case 13 => // Code
843848
val language = readLiteralString()

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,12 @@ class BASTWriter(val writer: ByteBufferWriter, val stringTable: StringTable) {
945945
writer.writeU8(12) // Let statement
946946
writeLocation(s.loc)
947947
writeIdentifier(s.identifier)
948+
s.typeRef match
949+
case Some(tr) =>
950+
writer.writeU8(1)
951+
writePathIdentifier(tr.pathId)
952+
case None =>
953+
writer.writeU8(0)
948954
writeLiteralString(s.expression)
949955
}
950956

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ package object bast {
5858
* with revision 0 (pre-check era) will be rejected with a
5959
* clear message.
6060
*/
61-
val FORMAT_REVISION: Short = 2
61+
val FORMAT_REVISION: Short = 3
6262

6363
/** Magic bytes for BAST file identification: "BAST" */
6464
val MAGIC_BYTES: Array[Byte] = Array('B'.toByte, 'A'.toByte, 'S'.toByte, 'T'.toByte)

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,10 @@ private[parsing] trait StatementParser {
114114

115115
private def letStatement[u: P]: P[LetStatement] = {
116116
P(
117-
Index ~ Keywords.let ~/ identifier ~ Punctuation.equalsSign ~/ literalString ~/ Index
118-
)./.map { case (start, id, expr, end) =>
119-
LetStatement(at(start, end), id, expr)
117+
Index ~ Keywords.let ~/ identifier ~ (Punctuation.colon ~ typeRef).? ~
118+
Punctuation.equalsSign ~/ literalString ~/ Index
119+
)./.map { case (start, id, optTypeRef, expr, end) =>
120+
LetStatement(at(start, end), id, optTypeRef, expr)
120121
}
121122
}
122123

language/shared/src/test/scala/com/ossuminc/riddl/language/ASTTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class ASTTest extends AbstractTestingBasis {
138138
BecomeStatement(At.empty, entityRef, HandlerRef(At(), PathIdentifier(At(), Seq("Entity")))),
139139
CodeStatement(At.empty, language = LiteralString(At.empty, "scala"), body = "def f[A](x: A): A"),
140140
ErrorStatement(At.empty, LiteralString(At.empty, "error message")),
141-
LetStatement(At.empty, Identifier(At.empty, "varName"), LiteralString(At.empty, "value")),
141+
LetStatement(At.empty, Identifier(At.empty, "varName"), None, LiteralString(At.empty, "value")),
142142
MatchStatement(At.empty, LiteralString(At.empty, "expr"), Seq(MatchCase(At.empty, LiteralString(At.empty, "pattern"), Contents.empty())), Contents.empty()),
143143
MorphStatement(At.empty, entityRef, StateRef(At.empty, PathIdentifier(At(), Seq("state"))), messageRef),
144144
SendStatement(At.empty, messageRef, InletRef(At.empty, PathIdentifier(At.empty, Seq("inlet")))),

language/shared/src/test/scala/com/ossuminc/riddl/language/parsing/StatementsTest.scala

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,66 @@ abstract class StatementsTest(using PlatformContext) extends AbstractParsingTest
103103
"check Let Statement" in { td =>
104104
val id = Identifier(At.empty, "foo")
105105
val expr = LiteralString(At.empty, "value")
106-
val s = LetStatement(At.empty, id, expr)
106+
val s = LetStatement(At.empty, id, None, expr)
107107
s.kind must be("Let Statement")
108108
s.format must be(s"let ${id.format} = ${expr.format}")
109109
checkStatement(s)
110110
}
111+
"check Let Statement with type annotation" in { td =>
112+
val id = Identifier(At.empty, "foo")
113+
val tr = TypeRef(At.empty, "type", PathIdentifier(At.empty, Seq("Number")))
114+
val expr = LiteralString(At.empty, "42")
115+
val s = LetStatement(At.empty, id, Some(tr), expr)
116+
s.kind must be("Let Statement")
117+
s.format must be("let foo: type Number = \"42\"")
118+
checkStatement(s)
119+
}
120+
"parse Let Statement with type annotation" in { (td: TestData) =>
121+
val input = RiddlParserInput(
122+
"""domain LetTest is {
123+
| context LetTest is {
124+
| type MyCommand is command { field: String }
125+
| handler h is {
126+
| on init {
127+
| let myVar: MyCommand = "MyCommand(field = hello)"
128+
| }
129+
| }
130+
| }
131+
|}""".stripMargin, td)
132+
TopLevelParser.parseInput(input) match
133+
case Left(messages) => fail(messages.justErrors.format)
134+
case Right(root) =>
135+
val clause = AST.getContexts(AST.getTopLevelDomains(root).head).head.handlers.head.clauses.head
136+
val s: Statement = clause.contents.filter[Statement].head
137+
s.isInstanceOf[LetStatement] must be(true)
138+
val letStmt = s.asInstanceOf[LetStatement]
139+
letStmt.identifier.value must be("myVar")
140+
letStmt.typeRef must not be empty
141+
letStmt.typeRef.get.pathId.value must be(Seq("MyCommand"))
142+
letStmt.expression.s must be("MyCommand(field = hello)")
143+
}
144+
"parse Let Statement without type annotation" in { (td: TestData) =>
145+
val input = RiddlParserInput(
146+
"""domain LetTest2 is {
147+
| context LetTest2 is {
148+
| handler h is {
149+
| on init {
150+
| let myVar = "some value"
151+
| }
152+
| }
153+
| }
154+
|}""".stripMargin, td)
155+
TopLevelParser.parseInput(input) match
156+
case Left(messages) => fail(messages.justErrors.format)
157+
case Right(root) =>
158+
val clause = AST.getContexts(AST.getTopLevelDomains(root).head).head.handlers.head.clauses.head
159+
val s: Statement = clause.contents.filter[Statement].head
160+
s.isInstanceOf[LetStatement] must be(true)
161+
val letStmt = s.asInstanceOf[LetStatement]
162+
letStmt.identifier.value must be("myVar")
163+
letStmt.typeRef must be(None)
164+
letStmt.expression.s must be("some value")
165+
}
111166
"check Code Statement" in { td =>
112167
val language = LiteralString(At.empty, "scala")
113168
val body = "for { i <- collection } yield { i.that }"
Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
passes/input/check/adaptor-direction/adaptor-direction.riddl(12:9->34):
22
Inbound Adaptor 'BadInbound' handles Command 'SourceCtx.DoIt' from Context 'SourceCtx', but inbound adaptors should handle events and results (the target's output):
33
on command SourceCtx.DoIt {
4-
passes/input/check/adaptor-direction/adaptor-direction.riddl(12:9->34):
5-
Processing for commands should result in sending an event:
6-
on command SourceCtx.DoIt {
74
passes/input/check/adaptor-direction/adaptor-direction.riddl(22:9->34):
85
Outbound Adaptor 'BadOutbound' handles Event 'SourceCtx.ItDone' from Context 'SourceCtx', but outbound adaptors should handle commands and queries (the target's input):
96
on event SourceCtx.ItDone {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
package com.ossuminc.riddl.passes.validate
88

99
import com.ossuminc.riddl.language.AST.Root
10-
import com.ossuminc.riddl.language.Messages.{Messages, StyleWarning}
10+
import com.ossuminc.riddl.language.Messages.{Messages, Warning}
1111
import com.ossuminc.riddl.language.Messages
1212
import com.ossuminc.riddl.language.parsing.RiddlParserInput
1313
import com.ossuminc.riddl.utils.{pc, ec}
@@ -47,8 +47,8 @@ class StatementValidatorTest extends AbstractValidatingTest {
4747
val warnings = messages.justWarnings
4848
warnings.isEmpty mustBe false
4949
messages.exists { (msg: Messages.Message) =>
50-
msg.kind == StyleWarning &&
51-
msg.message.contains("Cross-context references are ill-advised")
50+
msg.kind == Warning &&
51+
msg.message.contains("Cross-context references violate")
5252
} must be(true)
5353
}
5454

0 commit comments

Comments
 (0)