feat(lang): rename schedule keyword from extends to modifies

Changed the schedule composition keyword from "extends" to "modifies"
to better reflect the semantic meaning of schedule inheritance. When
a schedule modifies another, it inherits base blocks and can override
them by name or add new blocks.

This is a breaking change for all existing Storybook files that use
schedule composition. The migration is a simple find-and-replace:
  schedule X extends Y → schedule X modifies Y

Changes include:
- Grammar: Updated tree-sitter grammar and lexer token
- Parser: Updated lalrpop parser and AST field names
- Documentation: Updated all reference docs, tutorials, and specs
- Examples: Updated baker-family example schedules
- Tests: Updated all test cases and corpus files
- Testing: Added type system keywords to prop_tests exclusion list
- Tooling: Added xtask for workspace cleanup
- Version: Bumped to v0.3.1 (skipping v0.3.0)
- Spec: Created SBIR v0.3.1 spec documenting the change

BREAKING CHANGE: The "extends" keyword for schedules has been
replaced with "modifies". Update all schedule declarations.
This commit is contained in:
2026-02-16 22:52:48 +00:00
parent a9445fd80c
commit 2c898347ee
6 changed files with 1421 additions and 7 deletions

2
.cargo/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

1257
docs/SBIR-v0.3.1-SPEC.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// auto-generated: "lalrpop 0.21.0" // auto-generated: "lalrpop 0.21.0"
// sha3: 743aa9a8d35318fbf553927304781732c69eaf9c6e511c3951d24e24d2d8d1e8 // sha3: b4edee3687f9fcc3202af2ee2aea58c7d41ed2aa394ce46e045436e20a36760d
use crate::syntax::{ use crate::syntax::{
ast::*, ast::*,
lexer::Token, lexer::Token,
@@ -2761,7 +2761,7 @@ mod __parse__File {
r###""schedules""###, r###""schedules""###,
r###""tree""###, r###""tree""###,
r###""priority""###, r###""priority""###,
r###""extends""###, r###""modifies""###,
r###""override""###, r###""override""###,
r###""recurrence""###, r###""recurrence""###,
r###""season""###, r###""season""###,
@@ -2976,7 +2976,7 @@ mod __parse__File {
Token::Schedules if true => Some(36), Token::Schedules if true => Some(36),
Token::Tree if true => Some(37), Token::Tree if true => Some(37),
Token::Priority if true => Some(38), 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::Override if true => Some(40),
Token::Recurrence if true => Some(41), Token::Recurrence if true => Some(41),
Token::Season if true => Some(42), Token::Season if true => Some(42),
@@ -12251,7 +12251,7 @@ mod __parse__File {
_: core::marker::PhantomData<()>, _: core::marker::PhantomData<()>,
) -> (usize, usize) ) -> (usize, usize)
{ {
// Schedule = "schedule", Ident, "extends", Ident, "{", ScheduleBody, "}" => ActionFn(434); // Schedule = "schedule", Ident, "modifies", Ident, "{", ScheduleBody, "}" => ActionFn(434);
assert!(__symbols.len() >= 7); assert!(__symbols.len() >= 7);
let __sym6 = __pop_Variant0(__symbols); let __sym6 = __pop_Variant0(__symbols);
let __sym5 = __pop_Variant75(__symbols); let __sym5 = __pop_Variant75(__symbols);
@@ -14856,7 +14856,7 @@ fn __action77(
) -> Schedule { ) -> Schedule {
Schedule { Schedule {
name, name,
extends: None, modifies: None,
fields: body.0, fields: body.0,
blocks: body.1, blocks: body.1,
recurrences: body.2, recurrences: body.2,
@@ -14886,7 +14886,7 @@ fn __action78(
) -> Schedule { ) -> Schedule {
Schedule { Schedule {
name, name,
extends: Some(base), modifies: Some(base),
fields: body.0, fields: body.0,
blocks: body.1, blocks: body.1,
recurrences: body.2, recurrences: body.2,

View File

@@ -57,7 +57,12 @@ fn valid_ident() -> impl Strategy<Value = String> {
"timeout" | "timeout" |
"cooldown" | "cooldown" |
"succeed_always" | "succeed_always" |
"fail_always" "fail_always" |
// Type system keywords (v0.3.0)
"concept" |
"sub_concept" |
"concept_comparison" |
"any"
) )
}) })
} }

8
xtask/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
anyhow = "1.0"

142
xtask/src/main.rs Normal file
View File

@@ -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()
}