diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..35049cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/docs/SBIR-v0.3.1-SPEC.md b/docs/SBIR-v0.3.1-SPEC.md new file mode 100644 index 0000000..79175f0 --- /dev/null +++ b/docs/SBIR-v0.3.1-SPEC.md @@ -0,0 +1,1257 @@ +# Storybook Intermediate Representation (SBIR) v0.3.1 Specification + +**Version:** 0.3.1 +**Status:** Draft +**Date:** February 2026 + +--- + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [File Format Overview](#2-file-format-overview) +3. [Section 1: Header](#3-section-1-header) +4. [Section 2: String Table](#4-section-2-string-table) +5. [Section 3: Types](#5-section-3-types) +6. [Section 4: Characters](#6-section-4-characters) +7. [Section 5: Templates](#7-section-5-templates) +8. [Section 6: Species](#8-section-6-species) +9. [Section 7: Behaviors](#9-section-7-behaviors) +10. [Section 8: Schedules](#10-section-8-schedules) +11. [Section 9: Institutions](#11-section-9-institutions) +12. [Section 10: Relationships](#12-section-10-relationships) +13. [Section 11: Locations](#13-section-11-locations) +14. [Section 12: Life Arcs](#14-section-12-life-arcs) +15. [Section 13: Enums](#15-section-13-enums) +16. [Changelog](#16-changelog) + +--- + +## 1. Introduction + +### 1.1 Purpose + +The Storybook Intermediate Representation (SBIR) is a binary format that represents compiled Storybook programs. It serves as the interchange format between the Storybook compiler and runtime engines. + +### 1.2 Design Goals + +- **Compact:** Efficient binary encoding for large story worlds +- **Fast to load:** Direct memory mapping when possible +- **Versioned:** Clear version tracking for format evolution +- **Complete:** Represents all semantic information from source +- **Runtime-ready:** Minimal post-processing required + +### 1.3 Changes in v0.3.1 + +**Keyword changes:** +1. **Schedule Composition** - The `extends` keyword for schedule inheritance has been renamed to `modifies` + - Old: `schedule BakerSchedule extends WorkWeek { ... }` + - New: `schedule BakerSchedule modifies WorkWeek { ... }` + - This is a **find-and-replace** change - no binary format changes required + - The field name in the Schedule struct remains the same (just stores the base schedule name) + +### 1.4 Changes in v0.3.0 + +**Major additions:** +1. **Type System** - Concepts, sub-concepts (enum/record), and concept comparisons with pattern matching +2. **Species-Based Template Inheritance** - Templates can declare a species base for field inheritance +3. **Life Arc Field Requirements** - Life arcs can declare required fields with type annotations + +**Breaking changes:** +- TYPES section now populated with concept, sub_concept, and concept_comparison definitions +- Value discriminants renamed: Int→Number, Float→Decimal, String→Text, Bool→Boolean +- Expression discriminants renamed: IntLit→NumberLit, FloatLit→DecimalLit, StringLit→TextLit, BoolLit→BooleanLit +- TEMPLATES section extended with species_base field +- LIFE ARCS section extended with required_fields + +### 1.5 Changes in v0.2.0 + +**Major additions:** +1. **Resource Linking System** - Characters and institutions can link to behaviors and schedules with conditions and priorities +2. **Year-Long Schedule System** - Schedules support temporal patterns (day-specific, seasonal, recurrence) +3. **Behavior Tree Enhancements** - Named nodes, decorator parameters, keyword transformations + +**Breaking changes:** +- CHARACTERS section extended with behavior_links and schedule_links +- INSTITUTIONS section extended with behavior_links and schedule_links +- SCHEDULES section redesigned with patterns and inheritance +- BEHAVIORS section extended with named nodes and parameterized decorators + +--- + +## 2. File Format Overview + +### 2.1 File Structure + +``` +[Header] +[String Table] +[Type Definitions] +[Characters Section] +[Templates Section] +[Species Section] +[Behaviors Section] +[Schedules Section] +[Institutions Section] +[Relationships Section] +[Locations Section] +[Life Arcs Section] +[Enums Section] +``` + +### 2.2 Primitive Types + +| Type | Size | Description | +|------|------|-------------| +| `u8` | 1 byte | Unsigned 8-bit integer | +| `u16` | 2 bytes | Unsigned 16-bit integer (little-endian) | +| `u32` | 4 bytes | Unsigned 32-bit integer (little-endian) | +| `u64` | 8 bytes | Unsigned 64-bit integer (little-endian) | +| `i32` | 4 bytes | Signed 32-bit integer (little-endian) | +| `i64` | 8 bytes | Signed 64-bit integer (little-endian) | +| `f32` | 4 bytes | IEEE 754 single-precision float | +| `f64` | 8 bytes | IEEE 754 double-precision float | +| `bool` | 1 byte | 0 = false, 1 = true | +| `String` | Variable | Length-prefixed UTF-8: `u32 length` + `[u8; length]` | +| `StringRef` | 4 bytes | Index into string table (u32) | + +### 2.3 Common Structures + +#### Option + +Optional values are encoded with a discriminant byte followed by the value if present: + +``` +u8 discriminant: + 0 = None → No additional bytes, next field starts immediately + 1 = Some → T data follows immediately after discriminant +``` + +**Encoding:** +- **None case:** Just 1 byte (0x00), nothing else +- **Some case:** 1 byte (0x01) + full T encoding + +**Examples:** + +`Option` when None: +``` +0x00 ← 1 byte total +``` + +`Option` when Some(42): +``` +0x01 ← discriminant (Some) +0x2A 0x00 0x00 0x00 ← StringRef = 42 (u32) + ← 5 bytes total +``` + +`Option>` when None: +``` +0x00 ← 1 byte total +``` + +`Option>` when Some([field1, field2]): +``` +0x01 ← discriminant (Some) +0x02 0x00 0x00 0x00 ← count = 2 (u32) +[Field 1 encoding] ← First field +[Field 2 encoding] ← Second field + ← 5+ bytes (depends on field sizes) +``` + +#### Vec +``` +u32 count +[T; count] +``` + +### 2.4 Binary Format Conventions + +**IMPORTANT: SBIR is a packed binary format with no separators.** + +#### No Delimiter Bytes + +All data is laid out **sequentially in memory** with **no separator characters** between: +- Items in a Vec +- Fields in a struct +- Sections in the file +- Values of any kind + +The file is a continuous stream of bytes where each element's size determines where the next element begins. + +#### Length-Prefix Pattern + +All variable-length data uses a **length-prefix pattern**: + +1. **Length/count field** (u32) tells you "how much data is coming" +2. **Data bytes** - read exactly that amount +3. **Next field** starts immediately after (no gap, no separator) + +#### Reading Variable-Length Vec + +When `T` itself is variable-length, each item carries its own size information: + +**Example: Vec** +``` +u32 count = 3 ← "There are 3 strings" + String 1: + u32 length = 5 ← "First string is 5 bytes" + [u8; 5] = "hello" ← Read 5 bytes, next item starts immediately + String 2: + u32 length = 5 ← "Second string is 5 bytes" + [u8; 5] = "world" ← Read 5 bytes + String 3: + u32 length = 3 ← "Third string is 3 bytes" + [u8; 3] = "foo" ← Read 3 bytes +``` + +**Example: Vec** (complex variable-length structs) +``` +u32 count = 2 ← "There are 2 characters" + Character 1: + u32 name_len = 6 + [u8; 6] = "Martha" + u8 species_discriminant = 1 ← Option::Some + u32 species_ref = 10 + u32 fields_count = 2 + Field 1: (name_ref, value_discriminant, value_data) + Field 2: (name_ref, value_discriminant, value_data) + u32 template_refs_count = 1 + StringRef = 15 + u32 behavior_links_count = 0 + u32 schedule_links_count = 0 + Character 2: + (starts immediately after Character 1 ends) + u32 name_len = 5 + [u8; 5] = "David" + ... (continues) +``` + +#### Parsing Algorithm + +The parser reads **sequentially** using each length field to know how many bytes to consume: + +``` +position = 0 +while position < file_size: + read_length_or_discriminant() + read_exactly_that_many_bytes() + position += bytes_read + // Next field starts here (no seeking, no separator scanning) +``` + +#### Key Rules + +1. **Fixed-size types** (u8, u32, f64, etc.) take their exact size - no padding +2. **Variable-size types** always start with a length prefix (u32) +3. **Option** starts with a discriminant byte (0=None, 1=Some) +4. **Enums/discriminated unions** start with a discriminant byte +5. **No alignment padding** - all data is tightly packed + +There are no: + +- No newline characters (`\n`) +- No separators (`,`, `;`, spaces) +- No null terminators (except inside UTF-8 string data) +- No padding bytes between fields (unless explicitly specified) +- No section markers or headers (sections just follow each other) + +The documentation uses newlines and indentation for **readability only** - the actual binary file is a continuous stream of bytes with no whitespace. + +--- + +## 3. Section 1: Header + +``` +Magic: [u8; 4] // "SBIR" (0x53 0x42 0x49 0x52) +Version: u16 // Major version (0x0003 for v0.3.0) +MinorVersion: u16 // Minor version (0x0000) +Flags: u32 // Reserved (0x00000000) +SectionCount: u32 // Number of sections (currently 13) +``` + +**Version History:** +- v0.1.0: Implicit version (pre-release) +- v0.2.0: First formal versioned release +- v0.3.0: Type system, species inheritance, life arc requirements + +--- + +## 4. Section 2: String Table + +The string table stores all strings used in the SBIR file. + +``` +Count: u32 +Strings: [String; Count] +``` + +**Usage:** All `StringRef` types reference an index in this table. + +**Encoding:** UTF-8 with length prefix. + +**Example:** +``` +Count: 3 +Strings: + [0]: "Alice" + [1]: "Wonderland" + [2]: "rabbit_hole" +``` + +--- + +## 5. Section 3: Types + +**Note:** New in v0.3.0. This section encodes the concept type system. + +### 5.1 Structure + +``` +ConceptCount: u32 +Concepts: [Concept; ConceptCount] +SubConceptCount: u32 +SubConcepts: [SubConcept; SubConceptCount] +ConceptComparisonCount: u32 +ConceptComparisons: [ConceptComparison; ConceptComparisonCount] +``` + +### 5.2 Concept Encoding + +``` +Concept: + name: StringRef +``` + +A concept is a named type declaration with no additional data. Sub-concepts reference +their parent concept by name. + +### 5.3 SubConcept Encoding + +``` +SubConcept: + name: StringRef + parent_concept: StringRef + kind: u8 + data: +``` + +**Kind Discriminants:** +``` +0x01 = Enum +0x02 = Record +``` + +**Enum (0x01):** +``` +variants: Vec +``` + +**Record (0x02):** +``` +fields: Vec +``` + +Where `Field` is encoded as: +``` +Field: + name: StringRef + value: Value +``` + +### 5.4 ConceptComparison Encoding + +``` +ConceptComparison: + name: StringRef + concept_ref: StringRef // The concept being compared + sub_concept_ref: StringRef // The sub-concept (enum) being matched + arms: Vec +``` + +**ComparisonArm:** +``` +ComparisonArm: + variant: StringRef // Enum variant to match + fields: Vec // Field assignments for this arm + condition: Option // Optional guard condition +``` + +### 5.5 Binary Example + +**Source:** +```storybook +concept Cup +sub_concept Cup.Type { Small, Medium, Large } +sub_concept Cup.Material { weight: 100, fragile: true } + +concept_comparison CupDefaults for Cup matching Cup.Type { + Small { capacity: 200 } + Medium { capacity: 350 } + Large { capacity: 500 } +} +``` + +**Binary (conceptual):** +``` +ConceptCount: 1 +Concepts: + [0]: name = StringRef("Cup") + +SubConceptCount: 2 +SubConcepts: + [0]: SubConcept { + name: StringRef("Type") + parent_concept: StringRef("Cup") + kind: 0x01 (Enum) + variants: [StringRef("Small"), StringRef("Medium"), StringRef("Large")] + } + [1]: SubConcept { + name: StringRef("Material") + parent_concept: StringRef("Cup") + kind: 0x02 (Record) + fields: [ + Field { name: StringRef("weight"), value: Number(100) }, + Field { name: StringRef("fragile"), value: Boolean(true) } + ] + } + +ConceptComparisonCount: 1 +ConceptComparisons: + [0]: ConceptComparison { + name: StringRef("CupDefaults") + concept_ref: StringRef("Cup") + sub_concept_ref: StringRef("Type") + arms: [ + { variant: StringRef("Small"), fields: [("capacity", Number(200))], condition: None }, + { variant: StringRef("Medium"), fields: [("capacity", Number(350))], condition: None }, + { variant: StringRef("Large"), fields: [("capacity", Number(500))], condition: None } + ] + } +``` + +--- + +## 6. Section 4: Characters + +### 6.1 Structure + +``` +Count: u32 +Characters: [Character; Count] +``` + +### 6.2 Character Encoding + +``` +Character: + name: StringRef + species: Option + fields: Map + template_refs: Vec // Templates this character uses + behavior_links: Vec // NEW in v0.2.0 + schedule_links: Vec // NEW in v0.2.0 +``` + +### 6.3 BehaviorLink (NEW in v0.2.0) + +``` +BehaviorLink: + behavior_id: u32 // Index into BEHAVIORS section + priority: u8 // 0=Low, 1=Normal, 2=High, 3=Critical + condition: Option // Optional activation condition + is_default: bool // Default behavior (no condition) +``` + +**Priority Encoding:** +``` +enum Priority { + Low = 0, + Normal = 1, + High = 2, + Critical = 3, +} +``` + +**Selection Algorithm:** +1. Filter links where `condition` evaluates to true (or is None) +2. Sort by priority (descending) +3. Return highest priority link +4. If tie, use declaration order + +### 6.4 ScheduleLink (NEW in v0.2.0) + +``` +ScheduleLink: + schedule_id: u32 // Index into SCHEDULES section + condition: Option // Optional activation condition + is_default: bool // Default schedule (fallback) +``` + +**Selection Algorithm:** +1. Iterate schedule_links in order +2. Skip default links initially +3. Return first link where `condition` is true (or None) +4. If no match, use default link (if present) + +### 6.5 Value Encoding + +``` +Value: + discriminant: u8 + data: +``` + +**Discriminants:** +``` +0x01 = Number(i64) // Renamed from Int in v0.3.0 +0x02 = Decimal(f64) // Renamed from Float in v0.3.0 +0x03 = Text(StringRef) // Renamed from String in v0.3.0 +0x04 = Boolean(bool) // Renamed from Bool in v0.3.0 +0x05 = Range(Value, Value) +0x06 = Time(u8 hour, u8 minute, u8 second) +0x07 = Duration(u32 hours, u32 minutes, u32 seconds) +0x08 = Identifier(Vec) // Qualified path +0x09 = List(Vec) +0x0A = Object(Vec) +0x0B = ProseBlock(StringRef tag, String content) +0x0C = Override(...) +``` + +**Note:** The wire format (discriminant bytes 0x01-0x0C) is unchanged from v0.2.0. +Only the semantic names have been updated to match the Storybook language's type +terminology. + +### 6.6 Expression Encoding + +``` +Expression: + discriminant: u8 + data: +``` + +**Discriminants:** +``` +0x01 = NumberLit(i64) // Renamed from IntLit in v0.3.0 +0x02 = DecimalLit(f64) // Renamed from FloatLit in v0.3.0 +0x03 = TextLit(StringRef) // Renamed from StringLit in v0.3.0 +0x04 = BooleanLit(bool) // Renamed from BoolLit in v0.3.0 +0x05 = Identifier(Vec) +0x06 = FieldAccess(Box, StringRef) +0x07 = Comparison(Box, CompOp, Box) +0x08 = Logical(Box, LogicalOp, Box) +0x09 = Unary(UnaryOp, Box) +0x0A = Quantifier(QuantifierKind, StringRef var, Box collection, Box predicate) +``` + +**CompOp:** `u8` (0x01=Eq, 0x02=Ne, 0x03=Lt, 0x04=Le, 0x05=Gt, 0x06=Ge) + +**LogicalOp:** `u8` (0x01=And, 0x02=Or) + +**UnaryOp:** `u8` (0x01=Not, 0x02=Neg) + +**QuantifierKind:** `u8` (0x01=ForAll, 0x02=Exists) + +--- + +## 7. Section 5: Templates + +``` +Count: u32 +Templates: [Template; Count] + +Template: + name: StringRef + species_base: Option // NEW in v0.3.0 - Species base for field inheritance + strict: bool + includes: Vec + fields: Map +``` + +**Species Base (NEW in v0.3.0):** + +When `species_base` is `Some(ref)`, the template inherits fields from the referenced +species as its base layer. The override chain is: + +1. Species fields (base layer) +2. Included template fields (override species) +3. Template's own fields (override includes) +4. Character fields (override template) + +Last-one-wins semantics apply at each layer. Type invariance is enforced: a field's +type cannot change through the inheritance chain (e.g., a Number field in the species +cannot become a Text field in the template). + +--- + +## 8. Section 6: Species + +``` +Count: u32 +Species: [Species; Count] + +Species: + name: StringRef + includes: Vec + fields: Map +``` + +--- + +## 9. Section 7: Behaviors + +### 9.1 Structure + +``` +Count: u32 +Behaviors: [Behavior; Count] +``` + +### 9.2 Behavior Encoding + +``` +Behavior: + name: StringRef + root: BehaviorNode +``` + +### 9.3 BehaviorNode Encoding + +``` +BehaviorNode: + discriminant: u8 + data: +``` + +#### Node Type Discriminants + +``` +0x01 = Selector +0x02 = Sequence +0x03 = Condition +0x04 = Action +0x10 = DecoratorRepeat +0x11 = DecoratorRepeatN +0x12 = DecoratorRepeatRange +0x13 = DecoratorInvert +0x14 = DecoratorRetry +0x15 = DecoratorTimeout +0x16 = DecoratorCooldown +0x17 = DecoratorGuard +0x18 = DecoratorSucceedAlways +0x19 = DecoratorFailAlways +0x20 = SubTree +``` + +#### Selector Node (0x01) + +``` +label: Option // NEW in v0.2.0 +children: Vec +``` + +**Keyword Mapping:** `selector` or `choose` + +#### Sequence Node (0x02) + +``` +label: Option // NEW in v0.2.0 +children: Vec +``` + +**Keyword Mapping:** `sequence` or `then` + +#### Condition Node (0x03) + +``` +expression: Expression +``` + +**Keyword Mapping:** `if` or `when` + +#### Action Node (0x04) + +``` +name: StringRef +parameters: Vec +``` + +**Keyword Mapping:** No prefix (just action name) + +#### Decorator Nodes (0x10-0x19) + +**DecoratorRepeat (0x10):** +``` +child: Box +``` +Keyword: `repeat { ... }` + +**DecoratorRepeatN (0x11):** +``` +count: u32 +child: Box +``` +Keyword: `repeat(N) { ... }` + +**DecoratorRepeatRange (0x12):** +``` +min: u32 +max: u32 +child: Box +``` +Keyword: `repeat(min..max) { ... }` + +**DecoratorInvert (0x13):** +``` +child: Box +``` +Keyword: `invert { ... }` + +**DecoratorRetry (0x14):** +``` +max_attempts: u32 +child: Box +``` +Keyword: `retry(N) { ... }` + +**DecoratorTimeout (0x15):** +``` +milliseconds: u64 // Duration in milliseconds +child: Box +``` +Keyword: `timeout(duration) { ... }` +Example: `timeout(5s)`, `timeout(30m)`, `timeout(2h)` + +**DecoratorCooldown (0x16):** +``` +milliseconds: u64 // Cooldown period in milliseconds +child: Box +``` +Keyword: `cooldown(duration) { ... }` + +**DecoratorIf (0x17):** +``` +condition: Expression +child: Box +``` +Keyword: `if(condition) { ... }` + +**DecoratorSucceedAlways (0x18):** +``` +child: Box +``` +Keyword: `succeed_always { ... }` + +**DecoratorFailAlways (0x19):** +``` +child: Box +``` +Keyword: `fail_always { ... }` + +#### SubTree Node (0x20) + +``` +path: Vec // Qualified path to subtree +``` + +Keyword: `include path::to::subtree` + +--- + +## 10. Section 8: Schedules + +### 10.1 Structure + +``` +Count: u32 +Schedules: [Schedule; Count] +``` + +### 10.2 Schedule Encoding (REDESIGNED in v0.2.0) + +``` +Schedule: + name: StringRef + parent_schedule_id: Option // Index into SCHEDULES section (for inheritance) + blocks: Vec + patterns: Vec // Day-specific, seasonal, recurrence patterns +``` + +### 10.3 ScheduleBlock + +``` +ScheduleBlock: + name: StringRef // Required in v0.2.0 + start: u16 // Minutes since midnight (0-1439) + end: u16 // Minutes since midnight (0-1439) + behavior_ref: Option> // Reference to behavior (qualified path) + fields: Map +``` + +**Changes from v0.1.0:** +- `name` is now required (was optional) +- `behavior_ref` replaces `activity: String` +- Time is encoded as minutes since midnight + +### 10.4 SchedulePattern (NEW in v0.2.0) + +``` +SchedulePattern: + kind: u8 // Pattern discriminant + specification: Vec // Pattern-specific data + blocks: Vec // Blocks to apply when pattern matches +``` + +**Pattern Kind Discriminants:** +``` +0x01 = DayPattern +0x02 = SeasonPattern +0x03 = RecurrencePattern +``` + +#### DayPattern (0x01) + +``` +specification: + day_enum_value: StringRef // References user-defined DayOfWeek enum +``` + +Example: `on Fireday` → references "Fireday" from user's DayOfWeek enum + +#### SeasonPattern (0x02) + +``` +specification: + season_enum_values: Vec // Multiple seasons allowed +``` + +Example: `season (EarlySummer, LateSummer)` → references Season enum values + +#### RecurrencePattern (0x03) + +``` +specification: + name: StringRef // Recurrence name + spec: RecurrenceSpec +``` + +**RecurrenceSpec:** +``` +RecurrenceSpec: + discriminant: u8 + data: + +Discriminants: + 0x01 = Every(u32 days) // every N days + 0x02 = WeeklyOn(Vec) // weekly on [days] + 0x03 = MonthlyOnDay(u8 day) // monthly on day N + 0x04 = Annually(u8 month, u8 day) // annually on month/day +``` + +### 10.5 Runtime Schedule Evaluation + +**Algorithm:** +1. Resolve character's schedule via ScheduleLink (conditional selection) +2. Merge inherited schedules (parent → child, depth-first) +3. Evaluate patterns for current day/season +4. Overlay patterns on base blocks (later patterns override earlier ones) +5. Produce final 24-hour schedule +6. Return ordered list of (time, behavior_ref) pairs + +--- + +## 11. Section 9: Institutions + +### 11.1 Structure (EXTENDED in v0.2.0) + +``` +Count: u32 +Institutions: [Institution; Count] + +Institution: + name: StringRef + fields: Map + behavior_links: Vec // NEW in v0.2.0 + schedule_links: Vec // NEW in v0.2.0 +``` + +**Note:** BehaviorLink and ScheduleLink are identical to Character section (§6.3, §6.4) + +--- + +## 12. Section 10: Relationships + +``` +Count: u32 +Relationships: [Relationship; Count] + +Relationship: + name: StringRef + participants: Vec + fields: Map + +Participant: + role: Option + name: Vec // Qualified path + self_block: Option> + other_block: Option> +``` + +--- + +## 13. Section 11: Locations + +``` +Count: u32 +Locations: [Location; Count] + +Location: + name: StringRef + fields: Map +``` + +--- + +## 14. Section 12: Life Arcs + +``` +Count: u32 +LifeArcs: [LifeArc; Count] + +LifeArc: + name: StringRef + required_fields: Vec // NEW in v0.3.0 + states: Vec + +ArcState: + name: StringRef + on_enter: Option> + transitions: Vec + +Transition: + to: StringRef + condition: Expression +``` + +### 14.1 FieldRequirement (NEW in v0.3.0) + +``` +FieldRequirement: + name: StringRef // Field name + type_name: StringRef // Expected type (e.g., "Number", "Text") +``` + +**Purpose:** Life arcs can declare required fields that any character using the life arc +must have. This enables compile-time validation that characters provide the necessary +fields for state transitions and on_enter actions. + +**Example:** +```storybook +life_arc Career requires { skill_level: Number, title: Text } { + state Junior { ... } + state Senior { ... } +} +``` + +**Binary:** +``` +LifeArc: + name: StringRef("Career") + required_fields: [ + FieldRequirement { name: StringRef("skill_level"), type_name: StringRef("Number") }, + FieldRequirement { name: StringRef("title"), type_name: StringRef("Text") } + ] + states: [...] +``` + +--- + +## 15. Section 13: Enums + +### 15.1 Structure + +``` +Count: u32 +Enums: [EnumDecl; Count] +``` + +### 15.2 EnumDecl Encoding + +``` +EnumDecl: + name: StringRef // u32 index into string table + variants: Vec // Encoded as below +``` + +**Variants encoding (Vec):** +``` +u32 variant_count // Number of enum variants +[StringRef; variant_count] // Array of variant names (each is u32) +``` + +### 15.3 Binary Example + +**Source:** +```storybook +enum SkillLevel { + Novice, + Beginner, + Intermediate, + Advanced, + Expert, + Master +} +``` + +**Binary encoding:** +``` +EnumDecl: + name: StringRef = 42 // "SkillLevel" (4 bytes) + + u32 variant_count = 6 // 6 variants (4 bytes) + variants: + StringRef = 43 // "Novice" (4 bytes) + StringRef = 44 // "Beginner" (4 bytes) + StringRef = 45 // "Intermediate" (4 bytes) + StringRef = 46 // "Advanced" (4 bytes) + StringRef = 47 // "Expert" (4 bytes) + StringRef = 48 // "Master" (4 bytes) + +Total: 32 bytes (4 + 4 + 6*4) +``` + +### 15.4 Usage + +**Usage:** Enums are used for: +- Calendar definitions (DayOfWeek, Season, Month) +- Custom enumerated values +- Pattern matching in schedules + +**Note:** As of v0.3.0, enum-like types can also be defined via `sub_concept` with +enum kind (Section 5.3). The key distinction is that `sub_concept` enums are tied to a +parent concept and participate in the concept type system (concept comparisons, +exhaustiveness checking), while standalone enums in this section are untyped +enumerations used primarily for calendar patterns and simple value sets. + +**Standard Calendar Enums (optional):** +- `DayOfWeek`: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday +- `Season`: Spring, Summer, Fall, Winter +- `Month`: January, February, ..., December + +--- + +## 16. Changelog + +### v0.3.0 (February 2026) + +**Major Features:** +- Type system: concepts, sub-concepts (enum and record kinds), concept comparisons +- Species-based template inheritance with type invariance enforcement +- Life arc field requirements with type annotations +- Value/expression type renames aligned with Storybook language terminology + +**Breaking Changes:** +- TYPES section (Section 3) now populated with concept, sub_concept, and concept_comparison definitions +- Value discriminant names changed: Int→Number, Float→Decimal, String→Text, Bool→Boolean (wire format unchanged) +- Expression discriminant names changed: IntLit→NumberLit, FloatLit→DecimalLit, StringLit→TextLit, BoolLit→BooleanLit (wire format unchanged) +- TEMPLATES section: added `species_base: Option` field before `strict` +- LIFE ARCS section: added `required_fields: Vec` field after `name` + +**Note:** SBIR encoder/decoder implementation is deferred until SaberVM design is finalized. + +### v0.2.0 (February 2026) + +**Major Features:** +- Resource linking system for behaviors and schedules +- Year-long schedule patterns (day, season, recurrence) +- Schedule inheritance and composition +- Behavior tree keyword support (named nodes) +- Parameterized decorators (repeat, retry, timeout, cooldown, if) + +**Breaking Changes:** +- CHARACTERS section: added behavior_links, schedule_links +- INSTITUTIONS section: added behavior_links, schedule_links +- SCHEDULES section: complete redesign with patterns and inheritance +- BEHAVIORS section: added named node support + +**Deprecations:** +- None (first versioned release) + +**Bug Fixes:** +- N/A (first formal release) + +### v0.1.0 (Implicit, Pre-Release) + +**Initial format** (inferred from existing codebase): +- Basic entity storage (characters, templates, species) +- Simple schedules (time blocks with activities) +- Behavior trees (symbolic syntax) +- Relationships, locations, life arcs +- Enum support + +--- + +## Appendix A: Binary Encoding Examples + +### Example 1: Character with Resource Links + +**Source:** +```storybook +character Alice: Human { + age: 7 + + uses behavior: CuriousBehavior, when: self.location == Wonderland, priority: High + uses behavior: DefaultBehavior, default: true + + uses schedule: AdventureSchedule, when: self.in_wonderland + uses schedule: NormalSchedule, default: true +} +``` + +**Binary (conceptual):** +``` +Character: + name: StringRef(0) // "Alice" + species: Some(StringRef(1)) // "Human" + fields: [ + ("age", Number(7)) + ] + template_refs: [] + behavior_links: [ + BehaviorLink { + behavior_id: 3 + priority: 2 // High + condition: Some(Comparison(FieldAccess(...), Eq, Identifier(...))) + is_default: false + }, + BehaviorLink { + behavior_id: 5 + priority: 1 // Normal + condition: None + is_default: true + } + ] + schedule_links: [ + ScheduleLink { + schedule_id: 1 + condition: Some(FieldAccess(...)) + is_default: false + }, + ScheduleLink { + schedule_id: 2 + condition: None + is_default: true + } + ] +``` + +### Example 2: Schedule with Patterns + +**Source:** +```storybook +schedule WorkWeek modifies BaseSchedule { + block morning { 08:00 - 12:00: WorkTasks } + block lunch { 12:00 - 13:00: EatLunch } + block afternoon { 13:00 - 17:00: WorkTasks } + + on Friday { + override afternoon { 13:00 - 15:00: FinishWeek } + } + + season (Summer) { + override morning { 07:00 - 11:00: WorkEarly } + } +} +``` + +**Binary (conceptual):** +``` +Schedule: + name: StringRef(10) // "WorkWeek" + parent_schedule_id: Some(0) // BaseSchedule index + blocks: [ + ScheduleBlock { + name: StringRef(11) // "morning" + start: 480 // 08:00 in minutes + end: 720 // 12:00 in minutes + behavior_ref: Some(["WorkTasks"]) + fields: [] + }, + ScheduleBlock { + name: StringRef(12) // "lunch" + start: 720 + end: 780 + behavior_ref: Some(["EatLunch"]) + fields: [] + }, + ScheduleBlock { + name: StringRef(13) // "afternoon" + start: 780 + end: 1020 + behavior_ref: Some(["WorkTasks"]) + fields: [] + } + ] + patterns: [ + SchedulePattern { + kind: 0x01 // DayPattern + specification: StringRef(14) // "Friday" + blocks: [ + ScheduleBlock { + name: StringRef(13) // "afternoon" (override) + start: 780 + end: 900 + behavior_ref: Some(["FinishWeek"]) + fields: [] + } + ] + }, + SchedulePattern { + kind: 0x02 // SeasonPattern + specification: [StringRef(15)] // ["Summer"] + blocks: [ + ScheduleBlock { + name: StringRef(11) // "morning" (override) + start: 420 + end: 660 + behavior_ref: Some(["WorkEarly"]) + fields: [] + } + ] + } + ] +``` + +--- + +## Appendix B: File Size Estimates + +**Assumptions:** +- Average character: 500 bytes +- Average behavior tree: 1 KB +- Average schedule: 800 bytes +- 1000 characters, 500 behaviors, 300 schedules + +**Estimated size:** +- Characters: 500 KB +- Behaviors: 500 KB +- Schedules: 240 KB +- Other sections: 100 KB +- **Total: ~1.34 MB** + +**Compression:** Typical compression (gzip/zstd) achieves 60-70% reduction → ~400-500 KB + +--- + +## Appendix C: Version History + +| Version | Date | Major Changes | +|---------|------|---------------| +| 0.1.0 | (Implicit) | Initial format | +| 0.2.0 | Feb 2026 | Resource linking, year-long schedules, behavior keywords | +| 0.3.0 | Feb 2026 | Type system, species inheritance, life arc requirements | +| 0.3.1 | Feb 2026 | Schedule keyword change: `extends` → `modifies` (source-level only) | + +--- + +**END OF SPECIFICATION** diff --git a/src/syntax/parser.rs b/src/syntax/parser.rs index 87f3acd..41e415f 100644 --- a/src/syntax/parser.rs +++ b/src/syntax/parser.rs @@ -1,5 +1,5 @@ // auto-generated: "lalrpop 0.21.0" -// sha3: 743aa9a8d35318fbf553927304781732c69eaf9c6e511c3951d24e24d2d8d1e8 +// sha3: b4edee3687f9fcc3202af2ee2aea58c7d41ed2aa394ce46e045436e20a36760d use crate::syntax::{ ast::*, lexer::Token, @@ -2761,7 +2761,7 @@ mod __parse__File { r###""schedules""###, r###""tree""###, r###""priority""###, - r###""extends""###, + r###""modifies""###, r###""override""###, r###""recurrence""###, r###""season""###, @@ -2976,7 +2976,7 @@ mod __parse__File { Token::Schedules if true => Some(36), Token::Tree if true => Some(37), Token::Priority if true => Some(38), - Token::Extends if true => Some(39), + Token::Modifies if true => Some(39), Token::Override if true => Some(40), Token::Recurrence if true => Some(41), Token::Season if true => Some(42), @@ -12251,7 +12251,7 @@ mod __parse__File { _: core::marker::PhantomData<()>, ) -> (usize, usize) { - // Schedule = "schedule", Ident, "extends", Ident, "{", ScheduleBody, "}" => ActionFn(434); + // Schedule = "schedule", Ident, "modifies", Ident, "{", ScheduleBody, "}" => ActionFn(434); assert!(__symbols.len() >= 7); let __sym6 = __pop_Variant0(__symbols); let __sym5 = __pop_Variant75(__symbols); @@ -14856,7 +14856,7 @@ fn __action77( ) -> Schedule { Schedule { name, - extends: None, + modifies: None, fields: body.0, blocks: body.1, recurrences: body.2, @@ -14886,7 +14886,7 @@ fn __action78( ) -> Schedule { Schedule { name, - extends: Some(base), + modifies: Some(base), fields: body.0, blocks: body.1, recurrences: body.2, diff --git a/src/syntax/prop_tests.rs b/src/syntax/prop_tests.rs index 360f8e3..01d5005 100644 --- a/src/syntax/prop_tests.rs +++ b/src/syntax/prop_tests.rs @@ -57,7 +57,12 @@ fn valid_ident() -> impl Strategy { "timeout" | "cooldown" | "succeed_always" | - "fail_always" + "fail_always" | + // Type system keywords (v0.3.0) + "concept" | + "sub_concept" | + "concept_comparison" | + "any" ) }) } diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..a933fb2 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +anyhow = "1.0" diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..c00e37d --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,142 @@ +use std::{ + env, + path::PathBuf, + process::Command, +}; + +use anyhow::{ + Context, + Result, +}; + +fn main() -> Result<()> { + let task = env::args().nth(1); + match task.as_deref() { + | Some("clean") => clean()?, + | Some(task) => { + eprintln!("Unknown task: {}", task); + print_help(); + std::process::exit(1); + }, + | None => { + print_help(); + std::process::exit(1); + }, + } + Ok(()) +} + +fn print_help() { + eprintln!( + r#" +Tasks: + clean Clean all build artifacts across all projects + "# + ); +} + +fn clean() -> Result<()> { + let root = project_root(); + + println!("🧹 Cleaning Storybook workspace...\n"); + + // Clean Rust projects + println!(" Cleaning Rust artifacts..."); + run_command(&["cargo", "clean"], &root)?; + + // Clean tree-sitter-storybook + let tree_sitter_dir = root.join("tree-sitter-storybook"); + if tree_sitter_dir.exists() { + println!(" Cleaning tree-sitter-storybook..."); + + // Remove node_modules + let node_modules = tree_sitter_dir.join("node_modules"); + if node_modules.exists() { + println!(" Removing node_modules/"); + std::fs::remove_dir_all(&node_modules) + .context("Failed to remove tree-sitter-storybook/node_modules")?; + } + + // Remove target directory + let target = tree_sitter_dir.join("target"); + if target.exists() { + println!(" Removing target/"); + std::fs::remove_dir_all(&target) + .context("Failed to remove tree-sitter-storybook/target")?; + } + + // Remove Cargo.lock + let cargo_lock = tree_sitter_dir.join("Cargo.lock"); + if cargo_lock.exists() { + println!(" Removing Cargo.lock"); + std::fs::remove_file(&cargo_lock) + .context("Failed to remove tree-sitter-storybook/Cargo.lock")?; + } + + // Remove build artifacts + let build_dir = tree_sitter_dir.join("build"); + if build_dir.exists() { + println!(" Removing build/"); + std::fs::remove_dir_all(&build_dir) + .context("Failed to remove tree-sitter-storybook/build")?; + } + } + + // Clean zed-storybook + let zed_dir = root.join("zed-storybook"); + if zed_dir.exists() { + println!(" Cleaning zed-storybook..."); + + // Remove grammars directory (build artifact) + let grammars = zed_dir.join("grammars"); + if grammars.exists() { + println!(" Removing grammars/"); + std::fs::remove_dir_all(&grammars) + .context("Failed to remove zed-storybook/grammars")?; + } + + // Remove extension.wasm + let wasm = zed_dir.join("extension.wasm"); + if wasm.exists() { + println!(" Removing extension.wasm"); + std::fs::remove_file(&wasm).context("Failed to remove zed-storybook/extension.wasm")?; + } + } + + // Clean mdbook artifacts + let docs_dir = root.join("docs"); + if docs_dir.exists() { + println!(" Cleaning mdbook artifacts..."); + + let book_dir = docs_dir.join("book"); + if book_dir.exists() { + println!(" Removing docs/book/"); + std::fs::remove_dir_all(&book_dir).context("Failed to remove docs/book")?; + } + } + + println!("\n✨ Clean complete!"); + Ok(()) +} + +fn run_command(cmd: &[&str], cwd: &PathBuf) -> Result<()> { + let mut command = Command::new(cmd[0]); + command.args(&cmd[1..]).current_dir(cwd); + + let status = command + .status() + .with_context(|| format!("Failed to run command: {:?}", cmd))?; + + if !status.success() { + anyhow::bail!("Command failed: {:?}", cmd); + } + + Ok(()) +} + +fn project_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf() +}