Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions NOTEBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`
(deprecated `ParseResult<T>` 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<T>` renamed to
`RiddlResult<T>` 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

Expand All @@ -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`
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 │
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
}
}
}
Loading