Skip to content

Commit d9361af

Browse files
reidspencerclaude
andcommitted
Add BAST import syntax parsing (Phase 4 foundation)
- Add BASTImport AST node with path, namespace, and contents fields - Add BASTImport to RootContents type - Add import syntax parser: `import "file.bast" as namespace` - Add doBASTImport() stub in ParsingContext (loading happens later) - Add NODE_BAST_IMPORT tag and serialization in BASTWriter/Reader - Add BASTImport cases to ValidationPass, ResolutionPass, SymbolsPass - Use readability 'as' (not keyword) to avoid conflict with 'described as' Contents loading will be implemented in BASTLoadingPass. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d1de810 commit d9361af

11 files changed

Lines changed: 108 additions & 6 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) {
143143
case NODE_ENTITY => readEntityNode()
144144
case NODE_MODULE => readModuleNode()
145145
case NODE_INCLUDE => readIncludeNode()
146+
case NODE_BAST_IMPORT => readBASTImportNode()
146147

147148
// Types
148149
case NODE_TYPE => readTypeNode()
@@ -265,6 +266,15 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) {
265266
Include[RiddlValue](loc, origin, contents)
266267
}
267268

269+
private def readBASTImportNode(): BASTImport = {
270+
val loc = readLocation()
271+
val path = readLiteralString()
272+
val namespace = readIdentifier()
273+
// Contents are not stored in BAST - they're loaded dynamically
274+
// during BASTLoadingPass when this import is encountered
275+
BASTImport(loc, path, namespace)
276+
}
277+
268278
// ========== Type Definitions ==========
269279

270280
private def readTypeNode(): Type = {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ case class BASTWriter(input: PassInput, outputs: PassesOutput)(using pc: Platfor
7070
case n: Nebula => writeNebula(n)
7171
case r: Root => writeRoot(r)
7272
case i: Include[?] => writeInclude(i)
73+
case bi: BASTImport => writeBASTImport(bi)
7374

7475
// Vital definitions
7576
case d: Domain => writeDomain(d)
@@ -449,6 +450,15 @@ case class BASTWriter(input: PassInput, outputs: PassesOutput)(using pc: Platfor
449450
writeContents(i.contents)
450451
}
451452

453+
private def writeBASTImport(bi: BASTImport): Unit = {
454+
writer.writeU8(NODE_BAST_IMPORT)
455+
writeLocation(bi.loc)
456+
writeLiteralString(bi.path)
457+
writeIdentifier(bi.namespace)
458+
// Contents are not serialized - they're loaded dynamically
459+
// during BASTLoadingPass when this import is encountered
460+
}
461+
452462
// ========== Definition Serialization ==========
453463

454464
private def writeDomain(d: Domain): Unit = {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ package object bast {
8787
val NODE_INCLUDE: Byte = 33
8888
val NODE_SAGA_STEP: Byte = 34
8989
val NODE_SCHEMA: Byte = 35
90+
val NODE_BAST_IMPORT: Byte = 36
9091

9192
// Metadata nodes
9293
val NODE_DESCRIPTION: Byte = 40
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Test file for BAST import syntax
2+
import "utils/common-types.bast" as utils
3+
import "models/shared-domain.bast" as shared
4+
5+
domain MyApp is {
6+
// This domain would use imported types like:
7+
// type UserId is utils.UUID
8+
briefly "Application domain using imported BAST modules"
9+
}

language/jvm-native/src/test/scala/com/ossuminc/riddl/language/parsing/ParserTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class ParserTest extends ParsingTest with org.scalatest.Inside {
4646
case Left(errors) =>
4747
errors must not be empty
4848
errors.head.message must include(
49-
"Expected one of (\"/*\" | \"//\" | \"author\" | \"include\" | \"module\""
49+
"Expected one of (\"/*\" | \"//\" | \"author\" | \"import\" | \"include\" | \"module\""
5050
)
5151
case Right(_) => fail("'domainfoois' should not be recognized")
5252
}

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -798,8 +798,8 @@ object AST:
798798
/** Type of definitions that can occur in a [[Module]] */
799799
type ModuleContents = OccursInModule | Include[OccursInModule]
800800

801-
/** The root is a module that can have other modules */
802-
type RootContents = ModuleContents | Module
801+
/** The root is a module that can have other modules and BAST imports */
802+
type RootContents = ModuleContents | Module | BASTImport
803803

804804
/** Things that can occur in the "With" section of a leaf definition */
805805
type MetaData =
@@ -1058,6 +1058,36 @@ object AST:
10581058
override def toString: String = format
10591059
end Include
10601060

1061+
/** A top-level import of a BAST (Binary AST) file with a namespace alias.
1062+
*
1063+
* Imports bring in pre-compiled definitions from .bast files at the top level of a RIDDL file.
1064+
* The imported definitions are accessible via the namespace prefix.
1065+
*
1066+
* Syntax: `import "path/to/file.bast" as namespace`
1067+
*
1068+
* @param loc
1069+
* The location of the import statement in the source
1070+
* @param path
1071+
* The path to the .bast file to import
1072+
* @param namespace
1073+
* The namespace alias for accessing imported definitions
1074+
* @param contents
1075+
* The loaded Nebula contents from the BAST file (populated during parsing)
1076+
*/
1077+
case class BASTImport(
1078+
loc: At = At.empty,
1079+
path: LiteralString,
1080+
namespace: Identifier,
1081+
contents: Contents[NebulaContents] = Contents.empty[NebulaContents]()
1082+
) extends Container[NebulaContents]:
1083+
type ContentType = NebulaContents
1084+
1085+
override def isRootContainer: Boolean = false
1086+
1087+
def format: String = s"""import "${path.s}" as ${namespace.value}"""
1088+
override def toString: String = format
1089+
end BASTImport
1090+
10611091
/** Base trait of a reference to definitions that can accept a message directly via a reference
10621092
*
10631093
* @tparam T

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,30 @@ trait ParsingContext(using pc: PlatformContext) extends ParsingErrors {
8888
// importDomain(url)
8989
}
9090

91+
/** Parse a BAST import statement.
92+
*
93+
* This creates a BASTImport node with the path and namespace. The actual BAST
94+
* file loading happens later during a loading pass, avoiding circular dependencies
95+
* between language and bast modules.
96+
*
97+
* @param loc The location of the import statement
98+
* @param path The path to the .bast file
99+
* @param namespace The namespace alias for accessing imported definitions
100+
* @return A BASTImport node (contents populated later by BASTLoadingPass)
101+
*/
102+
def doBASTImport(
103+
loc: At,
104+
path: LiteralString,
105+
namespace: Identifier
106+
)(implicit ctx: P[?]): BASTImport = {
107+
// Validate the path ends with .bast
108+
if !path.s.endsWith(".bast") then
109+
warning(loc, s"Import path '${path.s}' should end with .bast extension")
110+
end if
111+
// Create the BASTImport node - actual loading happens in BASTLoadingPass
112+
BASTImport(loc, path, namespace)
113+
}
114+
91115
def doIncludeParsing[CT <: RiddlValue](loc: At, path: String, rule: P[?] => P[Seq[CT]])(implicit
92116
ctx: P[?]
93117
): Include[CT] = {

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ trait RootParser { this: ModuleParser & CommonParser & ParsingContext =>
2020
include[u, RootContents](rootContents(_))
2121
}
2222

23+
/** Parse a BAST import statement: `import "path/to/file.bast" as namespace` */
24+
private def bastImport[u: P]: P[BASTImport] = {
25+
P(Index ~ Keywords.import_ ~ literalString ~ as ~ identifier ~ Index).map {
26+
case (start, path, namespace, end) =>
27+
doBASTImport(at(start, end), path, namespace)
28+
}
29+
}
30+
2331
private def rootContent[u: P]: P[RootContents] = {
24-
P(moduleContent | module | rootInclude[u]).asInstanceOf[P[RootContents]]
32+
P(bastImport | moduleContent | module | rootInclude[u]).asInstanceOf[P[RootContents]]
2533
}
2634

2735
private def rootContents[u: P]: P[Seq[RootContents]] =

passes/shared/src/main/scala/com/ossuminc/riddl/passes/resolve/ResolutionPass.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ case class ResolutionPass(input: PassInput, outputs: PassesOutput)(using io: Pla
153153
}
154154
case cg: ContainedGroup =>
155155
associateUsage(cg, resolveARef[Group](cg.group, parents))
156+
case _: BASTImport => () // BAST imports are resolved in BASTLoadingPass
156157
case _: NonReferencableDefinitions => () // These can't be referenced
157158
case _: NonDefinitionValues => () // Neither can these values
158159
case _: Definition => () // abstract definition, can't be referenced

passes/shared/src/main/scala/com/ossuminc/riddl/passes/symbols/SymbolsPass.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ case class SymbolsPass(input: PassInput, outputs: PassesOutput)(using pc: Platfo
5555
def process(definition: RiddlValue, parents: ParentStack): Unit = {
5656
definition match {
5757
case _: Root => // Root doesn't have a name
58+
case _: BASTImport => // BAST imports don't go in symbol table
5859
case _: NonDefinitionValues => // none of these can have names
5960
case nv: Definition if nv.isAnonymous => // Nameless things, like includes, aren't stored
6061
case nv: Definition if nv.id.isEmpty => // Empty names are not stored

0 commit comments

Comments
 (0)