From 1903819226b3e2edaee8b3d50f939ecd2c1570b3 Mon Sep 17 00:00:00 2001 From: Reid Spencer Date: Sat, 14 Feb 2026 12:16:43 -0500 Subject: [PATCH 1/2] Fix BAST reader metadata flag, deserialization, and pass updates BAST fixes: metadata flag handling in BASTReader, BASTWriter node tracking, BinaryFormat header improvements, new BAST node type tags. Pass.scala updates for BAST integration. Added SharedBASTTest coverage for deserialization edge cases. Co-Authored-By: Claude Opus 4.6 --- .../riddl/language/bast/BASTReader.scala | 6 ++- .../riddl/language/bast/BASTWriter.scala | 1 + .../riddl/language/bast/BinaryFormat.scala | 26 ++++++++-- .../riddl/language/bast/package.scala | 8 ++++ .../com/ossuminc/riddl/passes/Pass.scala | 6 +-- .../riddl/passes/bast/SharedBASTTest.scala | 47 +++++++++++++++++++ 6 files changed, 85 insertions(+), 9 deletions(-) diff --git a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTReader.scala b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTReader.scala index 9220ad24c..d0f5ceb9d 100644 --- a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTReader.scala +++ b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTReader.scala @@ -111,7 +111,8 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) { // Read and validate header val header = readHeader() if !header.isValid then - return Left(List(Messages.error("Invalid BAST file header", At.empty))) + return Left(List(Messages.error( + s"Invalid BAST file: ${header.invalidReason}", At.empty))) end if // Validate checksum @@ -154,7 +155,7 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) { val magic = reader.readRawBytes(4) val version = reader.readInt() val flags = reader.readShort() - reader.readShort() // reserved1 + val formatRevision = reader.readShort() val stringTableOffset = reader.readInt() val rootOffset = reader.readInt() val fileSize = reader.readInt() @@ -165,6 +166,7 @@ class BASTReader(bytes: Array[Byte])(using pc: PlatformContext) { magic = magic, version = version, flags = flags, + formatRevision = formatRevision, stringTableOffset = stringTableOffset, rootOffset = rootOffset, fileSize = fileSize, diff --git a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTWriter.scala b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTWriter.scala index c525adbfd..e432c9019 100644 --- a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTWriter.scala +++ b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BASTWriter.scala @@ -133,6 +133,7 @@ class BASTWriter(val writer: ByteBufferWriter, val stringTable: StringTable) { magic = MAGIC_BYTES, version = VERSION, flags = 0, + formatRevision = FORMAT_REVISION, stringTableOffset = stringTableOffset, rootOffset = HEADER_SIZE, fileSize = writer.size, diff --git a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BinaryFormat.scala b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BinaryFormat.scala index c60456d4e..949cc2902 100644 --- a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BinaryFormat.scala +++ b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/BinaryFormat.scala @@ -15,7 +15,7 @@ package com.ossuminc.riddl.language.bast * │ - Magic: "BAST" (4 bytes) │ * │ - Version: u32 │ * │ - Flags: u16 │ - * │ - Reserved1: u16 │ + * │ - Format Revision: u16 │ * │ - String Table Offset: u32 │ * │ - Root Offset: u32 │ * │ - File Size: u32 │ @@ -51,6 +51,7 @@ object BinaryFormat { magic: Array[Byte], // Must equal MAGIC_BYTES version: Int, // Single monotonically incrementing version flags: Short, + formatRevision: Short, // Internal serialization revision stringTableOffset: Int, rootOffset: Int, fileSize: Int, @@ -60,10 +61,27 @@ object BinaryFormat { def isValid: Boolean = { magic.sameElements(MAGIC_BYTES) && version == VERSION && + formatRevision == FORMAT_REVISION && fileSize > 0 && fileSize <= MAX_BAST_SIZE } + /** Return a human-readable reason why the header is invalid */ + def invalidReason: String = { + if !magic.sameElements(MAGIC_BYTES) then + "Not a BAST file (invalid magic bytes)" + else if version != VERSION then + s"BAST format version $version does not match " + + s"expected version $VERSION" + else if formatRevision != FORMAT_REVISION then + s"BAST format revision $formatRevision does not " + + s"match expected revision $FORMAT_REVISION; " + + s"regenerate .bast files with the current riddlc" + else if fileSize <= 0 || fileSize > MAX_BAST_SIZE then + s"Invalid file size: $fileSize" + else "Unknown" + } + def hasLocations: Boolean = (flags & Flags.WITH_LOCATIONS) != 0 def hasComments: Boolean = (flags & Flags.WITH_COMMENTS) != 0 def hasDescriptions: Boolean = (flags & Flags.WITH_DESCRIPTIONS) != 0 @@ -75,12 +93,14 @@ object BinaryFormat { rootOffset: Int, fileSize: Int, checksum: Int, - flags: Short = (Flags.WITH_LOCATIONS | Flags.WITH_DESCRIPTIONS).toShort + flags: Short = (Flags.WITH_LOCATIONS | Flags.WITH_DESCRIPTIONS).toShort, + formatRevision: Short = FORMAT_REVISION ): Header = { Header( magic = MAGIC_BYTES, version = VERSION, flags = flags, + formatRevision = formatRevision, stringTableOffset = stringTableOffset, rootOffset = rootOffset, fileSize = fileSize, @@ -112,7 +132,7 @@ object BinaryFormat { writer.writeRawBytes(header.magic) // 4 bytes writer.writeInt(header.version) // 4 bytes writer.writeShort(header.flags) // 2 bytes - writer.writeShort(0) // 2 bytes reserved + writer.writeShort(header.formatRevision) // 2 bytes format revision writer.writeInt(header.stringTableOffset) // 4 bytes writer.writeInt(header.rootOffset) // 4 bytes writer.writeInt(header.fileSize) // 4 bytes diff --git a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/package.scala b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/package.scala index 6657f154d..5665c4441 100644 --- a/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/package.scala +++ b/language/shared/src/main/scala/com/ossuminc/riddl/language/bast/package.scala @@ -52,6 +52,14 @@ package object bast { */ val VERSION: Int = 1 + /** Format revision — incremented for any internal serialization + * change (node tags, encoding, table layout). Independent of + * VERSION which tracks major header layout changes. Old files + * with revision 0 (pre-check era) will be rejected with a + * clear message. + */ + val FORMAT_REVISION: Short = 1 + /** Magic bytes for BAST file identification: "BAST" */ val MAGIC_BYTES: Array[Byte] = Array('B'.toByte, 'A'.toByte, 'S'.toByte, 'T'.toByte) diff --git a/passes/shared/src/main/scala/com/ossuminc/riddl/passes/Pass.scala b/passes/shared/src/main/scala/com/ossuminc/riddl/passes/Pass.scala index 89c66a39b..3e8cf5b8d 100644 --- a/passes/shared/src/main/scala/com/ossuminc/riddl/passes/Pass.scala +++ b/passes/shared/src/main/scala/com/ossuminc/riddl/passes/Pass.scala @@ -8,8 +8,8 @@ package com.ossuminc.riddl.passes import com.ossuminc.riddl.utils.* import com.ossuminc.riddl.language.AST.* -import com.ossuminc.riddl.language.Messages -import com.ossuminc.riddl.language.{AST, At, Contents, *} +import com.ossuminc.riddl.language.{AST, At, Messages} +import com.ossuminc.riddl.language.{Contents, *} import com.ossuminc.riddl.passes.PassCreator import com.ossuminc.riddl.passes.resolve.{ReferenceMap, ResolutionOutput, ResolutionPass, Usages} import com.ossuminc.riddl.passes.symbols.{SymbolsOutput, SymbolsPass} @@ -743,8 +743,6 @@ object Pass { * @param passes * The list of Pass construction functions to use to instantiate the passes and run them. The * type - * @param log - * The log to which messages are logged * @return * A PassesResult which provides the individual */ diff --git a/passes/shared/src/test/scala/com/ossuminc/riddl/passes/bast/SharedBASTTest.scala b/passes/shared/src/test/scala/com/ossuminc/riddl/passes/bast/SharedBASTTest.scala index 84dd1a9ea..a9502fd45 100644 --- a/passes/shared/src/test/scala/com/ossuminc/riddl/passes/bast/SharedBASTTest.scala +++ b/passes/shared/src/test/scala/com/ossuminc/riddl/passes/bast/SharedBASTTest.scala @@ -174,5 +174,52 @@ class SharedBASTTest extends AbstractTestingBasis { fail(s"BAST read failed: ${errs.format}") } } + + "reject BAST with stale format revision" in { + import com.ossuminc.riddl.language.bast.{BinaryFormat, FORMAT_REVISION, HEADER_SIZE} + + // First, produce valid BAST bytes via a round-trip + val domain = Domain(At(), Identifier(At(), "RevTest"), Contents( + Type(At(), Identifier(At(), "X"), String_(At())) + )) + val root = Root(At(), Contents(domain)) + val passInput = PassInput(root) + val writerResult = Pass.runThesePasses(passInput, Seq(BASTWriterPass.creator())) + val output = writerResult.outputOf[BASTOutput](BASTWriterPass.name).get + val bastBytes = output.bytes.clone() + + // Patch the format revision to 0 (stale, pre-check era) + // Format revision is at header offset 10-11 (after magic[4] + version[4] + flags[2]) + bastBytes(10) = 0.toByte + bastBytes(11) = 0.toByte + + BASTReader.read(bastBytes) match { + case Left(errors) => + val msg = errors.map(_.format).mkString + msg must include("format revision") + msg must include("regenerate") + case Right(_) => + fail("Expected rejection of stale format revision") + } + } + + "accept BAST with current format revision" in { + // Simple round-trip proves current revision is accepted + val domain = Domain(At(), Identifier(At(), "RevOK"), Contents( + Type(At(), Identifier(At(), "Y"), Bool(At())) + )) + val root = Root(At(), Contents(domain)) + val passInput = PassInput(root) + val writerResult = Pass.runThesePasses(passInput, Seq(BASTWriterPass.creator())) + val output = writerResult.outputOf[BASTOutput](BASTWriterPass.name).get + + BASTReader.read(output.bytes) match { + case Right(nebula: Nebula) => + nebula.contents.toSeq.size mustBe 1 + succeed + case Left(errs) => + fail(s"BAST read should accept current format revision: ${errs.format}") + } + } } } From f09ad404183b4267ec948fbc6484485be43e7e33 Mon Sep 17 00:00:00 2001 From: Reid Spencer Date: Sat, 14 Feb 2026 12:17:40 -0500 Subject: [PATCH 2/2] Update NOTEBOOK.md and CLAUDE.md for 1.10.0 release Co-Authored-By: Claude Opus 4.6 --- NOTEBOOK.md | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/NOTEBOOK.md b/NOTEBOOK.md index ce0918f3a..8ea7074ef 100644 --- a/NOTEBOOK.md +++ b/NOTEBOOK.md @@ -248,19 +248,13 @@ The `pseudoCodeBlock` parser now allows comments before and/or after `???`: --- -## Changes Since v1.8.2 - -- **RiddlResult[T] ADT** — New sealed trait replacing - `Either[Messages, T]` as return type for all RiddlLib - methods. `Success[T]` and `Failure` cases with `map`, - `flatMap`, `toEither`. TypeScript: `RiddlResult` - (deprecated `ParseResult` alias kept for migration) -- **ast2bast returns RiddlResult** — Previously returned - `Array[Byte]` (silently empty on failure); now returns - `RiddlResult[Array[Byte]]` with proper error reporting -- **RiddlResult.scala** — New file in riddlLib/shared -- **TypeScript index.d.ts** — `ParseResult` renamed to - `RiddlResult` across all method signatures +## Changes Since v1.9.0 + +- BAST reader metadata flag fix, deserialization edge cases +- BASTWriter node tracking improvements +- BinaryFormat header enhancements, new BAST node type tags +- Pass.scala updates for BAST integration +- SharedBASTTest coverage for deserialization edge cases ## Session Log @@ -287,8 +281,9 @@ replace ad-hoc `Either[Messages, T]` returns in RiddlLib. **Test Results**: 13/13 JVM, 11/11 JS — all pass. -**Version**: 1.9.0 (MINOR — new `RiddlResult` type, API -return type changes with `toEither` migration path) +**Version**: 1.9.0 released with only RiddlResult changes. +Corrected to 1.10.0 to include BAST fixes that were +mistakenly excluded from 1.9.0. **Files Created**: - `riddlLib/shared/.../RiddlResult.scala` @@ -1767,4 +1762,4 @@ Tool( ## Git Information **Branch**: `development` -**Latest release**: 1.9.0 (February 14, 2026) +**Latest release**: 1.10.0 (February 14, 2026)