Skip to content

Commit c393dae

Browse files
reidspencerclaude
andcommitted
Integrate BAST module into language and passes, add auto-generation
Reorganized the standalone bast/ module: - Moved utility code (BASTWriter, BASTReader, BASTUtils, etc.) to language/bast/ - Moved BASTWriterPass to passes/ module - Removed bast/ module from build.sbt Added automatic BAST loading during parsing: - TopLevelParser checks for newer .bast files and loads them automatically - Supports BAST imports referenced in parsed files via BASTLoader Added --auto-generate-bast / -B CLI option: - Generates .bast files next to .riddl files after parsing - Works like Python's .pyc caching mechanism - Also available as 'auto-generate-bast' in config files Fixed bug where BAST header was not being written: - BASTWriterPass.finalize() return value was being ignored - Added finalizedBytes field to store and use the result Moved tests from bast/ to passes/: - BASTLoaderTest, BASTRoundTripTest, DeepASTComparison Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 43e1445 commit c393dae

23 files changed

Lines changed: 5850 additions & 46 deletions

File tree

bast/NOTEBOOK.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
## Current Status
44

5-
**BAST (Binary AST) serialization and import integration is complete through Phase 4.**
5+
**BAST (Binary AST) serialization is fully integrated into the language and passes modules.**
66

7-
The `bast/` module provides efficient binary serialization/deserialization of RIDDL AST nodes, enabling fast `import "module.bast"` functionality. All 19 tests pass.
7+
As of Jan 15, 2026, the standalone `bast/` module has been **removed from build.sbt** and the code has been reorganized:
8+
- **Utility code**`language/shared/src/main/scala/com/ossuminc/riddl/language/bast/`
9+
- **BASTWriterPass**`passes/shared/src/main/scala/com/ossuminc/riddl/passes/`
10+
11+
This enables BAST to work like Python's `.pyc` files - automatic loading from cache when available.
812

913
## Work Completed (Recent)
1014

@@ -16,12 +20,21 @@ The `bast/` module provides efficient binary serialization/deserialization of RI
1620
- [x] Support imports at root level AND inside domains
1721
- [x] BASTLoader utility to load and populate contents
1822
- [x] Path resolution verified: `ImportedDomain.SomeType` resolves correctly
19-
20-
## In Progress
21-
22-
**Phase 5: CLI & Testing**
23+
- [x] Phase 5: Module Reorganization & Auto-Generation (Jan 15, 2026)
24+
- [x] Removed standalone `bast/` module from build.sbt
25+
- [x] Split BASTWriter: utility class in `language/bast/`, Pass wrapper in `passes/`
26+
- [x] Added `BASTUtils.scala` with `checkForBastFile()`, `loadBAST()`, `tryLoadBastOrParseRiddl()`
27+
- [x] Integrated automatic BAST loading into `TopLevelParser.parseURL()` and `parseInput()`
28+
- [x] Added `--auto-generate-bast` / `-B` CLI option to riddlc
29+
- [x] Added `auto-generate-bast` config file option
30+
- [x] Implemented auto-generation in `Riddl.parse()` when option enabled
31+
32+
## Completed
33+
34+
**Phase 5: CLI & Auto-Generation**
2335
- [x] Add `riddlc bast-gen` command - 77ba7544
24-
- [ ] Add `--use-bast-cache` flag (auto-generate BAST during parsing)
36+
- [x] Add `--auto-generate-bast` flag (auto-generate BAST during parsing)
37+
- [x] Automatic BAST loading when .bast file exists and is newer than .riddl
2538
- [ ] Performance benchmarks (parse RIDDL vs load BAST)
2639
- [ ] Cross-platform tests (JS, Native)
2740

@@ -46,7 +59,7 @@ The `bast/` module provides efficient binary serialization/deserialization of RI
4659

4760
## Open Questions
4861

49-
- Should BAST files be auto-generated as a build cache, or explicitly created by users?
62+
- ~~Should BAST files be auto-generated as a build cache, or explicitly created by users?~~ **RESOLVED**: Both options available - explicit via `bast-gen` command, automatic via `--auto-generate-bast` flag
5063
- How should BAST versioning handle breaking format changes?
5164

5265
## Test Status
@@ -61,12 +74,23 @@ The `bast/` module provides efficient binary serialization/deserialization of RI
6174

6275
## Key Code Locations
6376

77+
**Language Module** (`language/shared/src/main/scala/com/ossuminc/riddl/language/bast/`):
6478
- `package.scala` - Constants, node type tags (1-255)
6579
- `BinaryFormat.scala` - Header spec, file structure
66-
- `BASTWriter.scala` - Serialization pass
80+
- `BASTWriter.scala` - Serialization utility class (non-Pass)
6781
- `BASTReader.scala` - Deserialization
6882
- `BASTLoader.scala` - Import loading utility
83+
- `BASTUtils.scala` - File checking, BAST loading helpers
84+
85+
**Passes Module** (`passes/shared/src/main/scala/com/ossuminc/riddl/passes/`):
86+
- `BASTWriterPass.scala` - Pass wrapper using AST traversal framework
87+
88+
**Commands Module** (`commands/jvm/src/main/scala/com/ossuminc/riddl/commands/`):
89+
- `BastGenCommand.scala` - `riddlc bast-gen` command
90+
91+
**Tests** (`passes/jvm/src/test/scala/com/ossuminc/riddl/passes/`):
6992
- `BASTLoaderTest.scala` - Import integration tests
93+
- `BASTRoundTripTest.scala` - Serialization round-trip tests
7094

7195
## Git Information
7296

build.sbt

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,6 @@ lazy val riddl: Project = Root("riddl", startYr = startYear, spdx ="Apache-2.0")
3232
language,
3333
languageNative,
3434
languageJS,
35-
bast,
36-
bastNative,
37-
bastJS,
3835
passes,
3936
passesNative,
4037
passesJS,
@@ -208,26 +205,6 @@ lazy val language = language_cp.jvm.dependsOn(utils)
208205
lazy val languageJS = language_cp.js.dependsOn(utilsJS)
209206
lazy val languageNative = language_cp.native.dependsOn(utilsNative)
210207

211-
val BAST = config("bast")
212-
lazy val bast_cp = CrossModule("bast", "riddl-bast")(JVM, JS, Native)
213-
.dependsOn(cpDep(language_cp), cpDep(passes_cp))
214-
.configure(With.typical, With.GithubPublishing)
215-
.settings(
216-
Test / parallelExecution := false,
217-
scalacOptions ++= Seq("-explain"),
218-
description := "Binary AST (BAST) serialization for fast import of RIDDL modules"
219-
)
220-
.jvmConfigure(With.coverage(70))
221-
.jvmConfigure(With.noMiMa)
222-
.jsConfigure(With.ScalaJS("RIDDL: bast", withCommonJSModule = true))
223-
.jsConfigure(With.noMiMa)
224-
.nativeConfigure(With.Native(mode = "fast"))
225-
.nativeConfigure(With.noMiMa)
226-
227-
lazy val bast = bast_cp.jvm.dependsOn(language, passes)
228-
lazy val bastJS = bast_cp.js.dependsOn(languageJS, passesJS)
229-
lazy val bastNative = bast_cp.native.dependsOn(languageNative, passesNative)
230-
231208
val Passes = config("passes")
232209
lazy val passes_cp = CrossModule("passes", "riddl-passes")(JVM, JS, Native)
233210
.dependsOn(cpDep(utils_cp), cpDep(language_cp))
@@ -354,7 +331,7 @@ val riddlLibNative = riddlLib_cp.native
354331

355332
val Commands = config("commands")
356333
lazy val commands_cp: CrossProject = CrossModule("commands", "riddl-commands")(JVM, Native)
357-
.dependsOn(cpDep(utils_cp), cpDep(language_cp), cpDep(passes_cp), cpDep(diagrams_cp), cpDep(bast_cp))
334+
.dependsOn(cpDep(utils_cp), cpDep(language_cp), cpDep(passes_cp), cpDep(diagrams_cp))
358335
.configure(With.typical, With.GithubPublishing)
359336
.settings(
360337
scalacOptions ++= Seq("-explain", "--explain-types", "--explain-cyclic", "--no-warnings"),

commands/jvm/src/main/scala/com/ossuminc/riddl/commands/BastGenCommand.scala

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66

77
package com.ossuminc.riddl.commands
88

9-
import com.ossuminc.riddl.bast.{BASTOutput, BASTWriter}
109
import com.ossuminc.riddl.command.{CommandOptions, PassCommand, PassCommandOptions}
1110
import com.ossuminc.riddl.language.Messages
1211
import com.ossuminc.riddl.language.Messages.Messages
13-
import com.ossuminc.riddl.passes.{PassCreators, PassesResult}
12+
import com.ossuminc.riddl.passes.{BASTOutput, BASTWriterPass, PassCreators, PassesResult}
1413
import com.ossuminc.riddl.utils.PlatformContext
1514
import org.ekrich.config.Config
1615
import scopt.OParser
@@ -72,7 +71,7 @@ class BastGenCommand(using pc: PlatformContext) extends PassCommand[BastGenComma
7271
}
7372

7473
override def getPasses(options: Options): PassCreators = {
75-
Seq(BASTWriter.creator())
74+
Seq(BASTWriterPass.creator())
7675
}
7776

7877
override def replaceInputFile(opts: Options, inputFile: Path): Options = {
@@ -88,7 +87,7 @@ class BastGenCommand(using pc: PlatformContext) extends PassCommand[BastGenComma
8887
case Left(errors) => Left(errors)
8988
case Right(result) =>
9089
// Get the BAST output
91-
result.outputOf[BASTOutput](BASTWriter.name) match {
90+
result.outputOf[BASTOutput](BASTWriterPass.name) match {
9291
case None =>
9392
Left(Messages.errors("BASTWriter did not produce output"))
9493
case Some(bastOutput) =>

commands/jvm/src/test/scala/com/ossuminc/riddl/commands/BastGenCommandTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
package com.ossuminc.riddl.commands
88

9-
import com.ossuminc.riddl.bast.{BASTReader, BASTOutput, HEADER_SIZE}
9+
import com.ossuminc.riddl.language.bast.HEADER_SIZE
1010
import com.ossuminc.riddl.language.AST.Domain
1111
import com.ossuminc.riddl.utils.{pc, PlatformContext}
1212
import org.scalatest.matchers.must.Matchers

commands/shared/src/main/scala/com/ossuminc/riddl/command/CommonOptionsHelper.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ object CommonOptionsHelper:
5353
private inline def max_parallel_parsing = "max-parallel-parsing"
5454
private inline def max_include_wait = "max-include-wait"
5555
private inline def warnings_are_fatal = "warnings-are-fatal"
56+
private inline def auto_generate_bast = "auto-generate-bast"
5657

5758
lazy val commonOptionsParser: OParser[Unit, CommonOptions] = {
5859
val builder: OParserBuilder[CommonOptions] = OParser.builder[CommonOptions]
@@ -144,6 +145,12 @@ object CommonOptionsHelper:
144145
.action((_, c) => c.copy(warningsAreFatal = true))
145146
.text(
146147
"Makes validation warnings fatal to encourage code perfection"
148+
),
149+
opt[Unit]('B', auto_generate_bast)
150+
.optional()
151+
.action((_, c) => c.copy(autoGenerateBAST = true))
152+
.text(
153+
"Automatically generate .bast files next to .riddl files after parsing (like Python's .pyc)"
147154
)
148155
)
149156
}
@@ -226,6 +233,8 @@ object CommonOptionsHelper:
226233
else default.maxIncludeWait
227234
val warningsAreFatal =
228235
if obj.hasPath(warnings_are_fatal) then obj.getBoolean(warnings_are_fatal) else default.warningsAreFatal
236+
val autoGenerateBAST =
237+
if obj.hasPath(auto_generate_bast) then obj.getBoolean(auto_generate_bast) else default.autoGenerateBAST
229238

230239
CommonOptions(
231240
showTimes,
@@ -244,7 +253,8 @@ object CommonOptionsHelper:
244253
noANSIMessages,
245254
maxParallelParsing,
246255
maxIncludeWait,
247-
warningsAreFatal
256+
warningsAreFatal,
257+
autoGenerateBAST
248258
)
249259
end commonOptionsReader
250260

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2019-2026 Ossum, Inc.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package com.ossuminc.riddl.language.bast
8+
9+
import com.ossuminc.riddl.language.AST.*
10+
import com.ossuminc.riddl.language.{At, Messages}
11+
import com.ossuminc.riddl.language.Messages.Messages
12+
import com.ossuminc.riddl.utils.{PlatformContext, URL}
13+
14+
import scala.collection.mutable
15+
import scala.concurrent.{Await, ExecutionContext}
16+
import scala.concurrent.duration.*
17+
import scala.util.{Failure, Success, Try}
18+
19+
/** Utility for loading BAST imports.
20+
*
21+
* This loads BAST files referenced by BASTImport nodes and populates their
22+
* contents field with the loaded Nebula contents. Imports can appear at the
23+
* root level or inside domains.
24+
*/
25+
object BASTLoader {
26+
27+
/** Result of loading BAST imports */
28+
case class LoadResult(
29+
loadedCount: Int,
30+
failedCount: Int,
31+
messages: Messages
32+
)
33+
34+
/** Load all BAST imports in a Root, including those inside domains.
35+
*
36+
* Finds all BASTImport nodes in the Root and its domains, loads the referenced
37+
* BAST files, and populates each BASTImport's contents field with the loaded
38+
* Nebula contents.
39+
*
40+
* @param root The Root containing BASTImport nodes to load
41+
* @param baseURL The base URL for resolving relative BAST file paths
42+
* @param pc The platform context for file loading
43+
* @return LoadResult with counts and any error messages
44+
*/
45+
def loadImports(root: Root, baseURL: URL)(using pc: PlatformContext): LoadResult = {
46+
val msgs = mutable.ListBuffer.empty[Messages.Message]
47+
var loaded = 0
48+
var failed = 0
49+
50+
def loadImport(bi: BASTImport): Unit = {
51+
loadSingleImport(bi, baseURL) match {
52+
case Right(nebula) =>
53+
// Copy Nebula contents into BASTImport contents
54+
nebula.contents.foreach { item =>
55+
bi.contents.append(item)
56+
}
57+
loaded += 1
58+
case Left(error) =>
59+
msgs += Messages.Message(
60+
bi.loc,
61+
s"Failed to load BAST import '${bi.path.s}': $error",
62+
Messages.Error
63+
)
64+
failed += 1
65+
}
66+
}
67+
68+
def processContents[T <: RiddlValue](contents: Contents[T]): Unit = {
69+
contents.foreach {
70+
case bi: BASTImport => loadImport(bi)
71+
case d: Domain => processContents(d.contents)
72+
case _ => () // Not a BASTImport or Domain, skip
73+
}
74+
}
75+
76+
processContents(root.contents)
77+
LoadResult(loaded, failed, msgs.toList)
78+
}
79+
80+
/** Load a single BAST import.
81+
*
82+
* @param bi The BASTImport to load
83+
* @param baseURL The base URL for resolving relative paths
84+
* @param pc The platform context
85+
* @return Either an error message or the loaded Nebula
86+
*/
87+
private def loadSingleImport(bi: BASTImport, baseURL: URL)(using pc: PlatformContext): Either[String, Nebula] = {
88+
Try {
89+
// Resolve the path relative to the base URL
90+
val bastURL = if URL.isValid(bi.path.s) then {
91+
URL(bi.path.s)
92+
} else {
93+
baseURL.parent.resolve(bi.path.s)
94+
}
95+
96+
// Load the BAST file bytes
97+
implicit val ec: ExecutionContext = pc.ec
98+
val future = pc.load(bastURL).map { data =>
99+
// Parse as BAST - note: data is loaded as String, convert to bytes
100+
val bytes = data.getBytes("ISO-8859-1") // Binary data preserved
101+
val reader = BASTReader(bytes)
102+
reader.read() // Returns Either[Messages, Nebula]
103+
}
104+
105+
// Wait for the result (with timeout)
106+
Await.result(future, 30.seconds)
107+
} match {
108+
case Success(Right(nebula)) => Right(nebula)
109+
case Success(Left(msgs)) => Left(msgs.map(_.format).mkString("; "))
110+
case Failure(ex) => Left(ex.getMessage)
111+
}
112+
}
113+
114+
/** Check if a Root has any unloaded BASTImport nodes.
115+
*
116+
* @param root The Root to check
117+
* @return true if there are BASTImport nodes with empty contents
118+
*/
119+
def hasUnloadedImports(root: Root): Boolean = {
120+
var found = false
121+
def checkContents[T <: RiddlValue](contents: Contents[T]): Unit = {
122+
if !found then
123+
contents.foreach {
124+
case bi: BASTImport => if bi.contents.isEmpty then found = true
125+
case d: Domain => checkContents(d.contents)
126+
case _ => ()
127+
}
128+
end if
129+
}
130+
checkContents(root.contents)
131+
found
132+
}
133+
134+
/** Get all BASTImport nodes from a Root, including those inside domains.
135+
*
136+
* @param root The Root to search
137+
* @return Sequence of BASTImport nodes
138+
*/
139+
def getImports(root: Root): Seq[BASTImport] = {
140+
val result = mutable.ListBuffer.empty[BASTImport]
141+
def collectFromContents[T <: RiddlValue](contents: Contents[T]): Unit = {
142+
contents.foreach {
143+
case bi: BASTImport => result += bi
144+
case d: Domain => collectFromContents(d.contents)
145+
case _ => ()
146+
}
147+
}
148+
collectFromContents(root.contents)
149+
result.toSeq
150+
}
151+
}

0 commit comments

Comments
 (0)