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
82 changes: 82 additions & 0 deletions docs/decisions.log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2733,3 +2733,85 @@ Architectural decisions go to [`adr/`](adr/) instead.
Guarding against `limits::` in a string would need a Rust parser, which
costs more than this boundary is worth; the trade is stated in the test
rather than left for someone to discover.

- 2026-09-02 — In the context of SWG-4A-06's level-2 lexer, facing spec
§5.11's contract that a retained token costs at most twelve bytes on
`wasm32`, we decided to **retain a kind and a span and slice the lexeme
from the source on demand**, to achieve a token that cannot silently grow
a heap allocation per lexeme, accepting that every consumer of a token's
text must carry the source alongside it. Prior art, since the technique is
not this task's invention: `rustc_lexer` emits tokens carrying a kind and
a length and leaves the text in the source buffer; `rust-analyzer`'s
`rowan` keeps green-tree tokens interned rather than owned per
occurrence; `logos` generates lexers whose token payload is a `Span` the
caller slices. What none of them supplies is the *bound* — twelve bytes is
§5.11's own derivation from `MAX_TOKENS` and the cockpit's `wasm32` heap,
and the compile-time assertion that enforces it is this task's. Level 1's
`String`-owning `Token` is untouched: it is frozen, it is not the level-2
representation, and the two now differ on purpose.

- 2026-09-02 — In the context of the level-1 parser meeting a `swang 2`
header once the build supports level 2, we decided to **refuse it in the
level-1 entry point with `SWG0401`**, to achieve §5.4's "routes to one of
them and never mixes them", accepting that a caller who reaches for the
frozen entry point with level-2 text gets a structural refusal rather than
a routing hint. Without the guard, `Level::new(2)` succeeded and a level-1
`Program` carried level 2, so the formatter emitted `swang 2` above a
`pattern` block — a document neither level would read back. `SWG0001` was
rejected for it: this build *does* support level 2, and §5.10 forbids one
number carrying a second meaning. The arm cannot fire for a `swang 1`
source, so Law A is untouched by its existence, and the frozen baseline
proves it rather than the claim resting on inspection.

- 2026-09-02 — In the context of SWG-4A-06 reading exactly one scalar, we
decided to **implement `SWG0505` and `SWG0506` for `ppqn` alone**, to
achieve a level-2 acceptance set that contains no text §6.6 declares
invalid, accepting that two codes belonging to 4A-07's scalar layer appear
one task early. The alternative was `parse("swang 2\n\nscore { ppqn 0 }")`
returning `Ok` — a permissive fallback in the accepted set of a level that
has not frozen, which 4A-07 would then have to *narrow*. Refusing early is
the cheaper mistake to correct: widening an unfrozen level is routine,
and the two checks are four lines scoped to the one word this slice reads.

- 2026-09-02 — In the context of SWG-4A-06's depth accounting, facing a
falsification probe that **survived**, we decided to **have the parse
spend a budget its caller owns**, to achieve a wiring that can be
falsified today rather than when 4A-08 makes nesting reachable, accepting
a crate-internal signature that hands the budget in. Deleting
`enter_block` from the level-2 parser was caught by nothing at all: the
minimal score has one block, so no end-to-end breach can witness the
counter. The witness now reads it after a refusal *inside* the block,
where a depth still held is proof of entry — a balanced parse returns to
zero whether the block was entered or not, and would have passed either
way. This is not a side channel: there is no second entry that can succeed
without spending the same budget, and `Level2ResourceLimits` still has
private fields and `declared()` as its only production constructor.


- 2026-09-02 — In the context of `ast::v1::Level::new` being pinned to level
1 while `LANGUAGE_LEVEL` rose to 2, we decided to **stop the level-1 AST's
refusal from quoting the build's supported range**, to achieve a public
error that describes the constraint the code actually enforces, accepting
that two neighbouring types now word "unsupported level" differently on
purpose. The message interpolated `LANGUAGE_LEVEL`, so refusing level 2
read `language level 2 is not supported (1..=2)` — a sentence that lists
the level it is refusing, and sends the reader hunting for a bug anywhere
but where it is. The two doc comments carried the same stale claim. This
is the same error the constructor bug was, one layer out: the level-1 tree
asking the *build* what it may spell. It may spell level 1 (§5.4), and the
header's own `SWG0001` remains the place where the build-wide range is
named, because there the range is the subject.

- 2026-09-03 — In the context of `syntax/document.rs` documenting a
delegation it did not implement, we decided to **make `parse_document`
literally `parse_document_with_source_map` with the map dropped**, to
achieve one level decision per build rather than two kept in step by hand,
accepting that a caller wanting no map still pays for one — the price
`v1::parse` already pays a layer down. The module held two `match level`
blocks while its doc said the two "cannot drift"; the property was real
but maintained by discipline. Reintroducing the duplicate is invisible to
behaviour — 287 tests agreed with it — because a fresh copy of a router
does agree, and agrees right up until someone edits one copy. So the
witness is lexical: the level is read once in that file. `v2::parse_exact`
lost its only caller and is deleted rather than kept for symmetry, since a
`pub(crate)` wrapper with nobody behind it is not an API.
48 changes: 47 additions & 1 deletion docs/swang/foundation-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ recorded in `decisions.log.md` if reversed.
| SWG-4A-03 | Writer: transport and master timeline | code | 4A-01 |
| SWG-4A-04 | Writer: tracks, voices, groups, atoms | code | 4A-03 |
| SWG-4A-05 | Writer: techniques, positions, evidence, losses *(done)* | code | 4A-04, CORE-01 |
| SWG-4A-06 | Parser skeleton and level/root dispatch | code | 4A-01, INF-04, INF-06 |
| SWG-4A-06 | Parser skeleton and level/root dispatch *(done)* | code | 4A-01, INF-04, INF-06 |
| SWG-4A-07 | Parser: exact scalar types | code | 4A-06 |
| SWG-4A-08 | Parser: structural tree | code | 4A-07, 4A-02 |
| SWG-4A-09 | Checked `ScoreBuilder` | code | 4A-08 |
Expand Down Expand Up @@ -833,6 +833,52 @@ Acceptance:
Preregistered falsification probe: *adding owned lexeme text to the
level-2 token* **must be CAUGHT**.

**Delivered.** `LANGUAGE_LEVEL` is 2. `syntax/document.rs` routes on the
level and holds no grammar of its own; `parser/v2.rs` owns the level-2
entry, `format/v2.rs` the level-2 canonical text. Level 1's entry point
gained a guard that fires only for a level it does not own, because without
it a `swang 2` header flowed into a level-1 `Program` and the formatter
emitted `swang 2` above a `pattern` block.

**What this slice accepts, exactly:** `score { ppqn <n> }`. `ppqn` is
`score`'s only `1` word and the structural blocks are `*`/`?`, so the
minimal empty score is a complete level-2 program rather than a stub.

**What it refuses, and who owns the refusal.** `master_bar`, `track`,
`source` and `loss` are real `score` words (§6.4b) owned by SWG-4A-08; they
are refused here with a message that says so, never skipped, because a
parser that ignores a word it does not implement is how exact text stops
being exact. Two registry codes are implemented, `SWG0505` and `SWG0506`,
and only for `ppqn` — the scalar layer proper stays 4A-07's. The alternative
was to accept `ppqn 0960` and `ppqn 0`, text §6.6 declares invalid, into the
accepted set of a level that has not frozen.

**The three inherited SWG-INF-06 obligations, discharged:**

- the budget is constructed in the level-2 entry and nowhere else, and
consulted before the work it bounds — source bytes pre-lex, tokens as each
is retained, depth as the block is entered. `level_two_budget_boundary.rs`
still passes, with `parser/v2.rs` and `parser/v2/lexer.rs` added to its
exempt list one at a time and the dispatcher deliberately left off it;
- `swang_parse` gains the dispatched arm, so a budget breach reaching a
fuzzed input is a typed `SWG0509`. Two deterministic breaches drive the
declared limits through the public path with no scaling: 16 MiB of
whitespace padding a *grammatically valid* score (so it would be accepted
if the byte check were not consulted first), and four million tokens plus
one spelled under the byte cap;
- the retained level-2 token is a kind and a `Span` — twelve bytes, no
`String`, text sliced from the source. The bound is a `const` assertion in
the lexer, so it travels to every target the crate builds for; the
preregistered probe (owned lexeme text) is **CAUGHT-BY-COMPILE** on
`wasm32-unknown-unknown`.

**Inherited by SWG-4A-08**, found by falsification rather than assumed: the
depth axis is wired but cannot breach end-to-end in a grammar with one
block. Deleting `enter_block` was caught by nothing until a witness read the
counter directly on the real parse. When the structural tree lands, the
depth cap becomes reachable from text and deserves an end-to-end breach of
its own.

### SWG-4A-07 — Parser: exact scalar types

**Kind:** code. **Depends on:** 4A-06
Expand Down
47 changes: 46 additions & 1 deletion fuzz/fuzz_targets/swang_parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,22 @@
//! `SWG\d{4}` and a span inside the source (`start <= end <= len`).
//! * On `Ok`: `format` emits canonical text that reparses to the same AST
//! (law 3) and is its own fixed point (law 2).
//! * `parse_document`: the dispatched path — level 1 or level 2 — is
//! `Ok(Document)` xor a non-empty `Vec<Diagnostic>`, every diagnostic
//! carries a registry code and an in-source span, and `format_document`
//! obeys the same two laws whichever level answered.
//!
//! SWG-INF-06 declared level 2's input bounds but could not honestly claim
//! an end-to-end breach oracle: public parsing could not reach a level-2
//! parser at all, so the oracle would have covered a path the binary could
//! not enter. SWG-4A-06 is the task that opens it, so the oracle lands
//! here. A budget breach is a typed `SWG0509` reaching the caller — not an
//! abort, not an allocation death, not a silent success.

use griff_swang::syntax::{format, header_level, parse, Diagnostic, LANGUAGE_LEVEL};
use griff_swang::syntax::{
format, format_document, header_level, parse, parse_document, Diagnostic, Document,
LANGUAGE_LEVEL,
};
use libfuzzer_sys::fuzz_target;

/// The one diagnostic contract, applied to the header pre-parser and the
Expand Down Expand Up @@ -74,4 +88,35 @@ fuzz_target!(|source: &str| {
}
}
}

match parse_document(source) {
Ok(document) => {
let canonical = format_document(&document);
let reparsed = parse_document(&canonical)
.unwrap_or_else(|d| panic!("canonical text must reparse (law 2): {d:?}"));
assert_eq!(
format_document(&reparsed),
canonical,
"format_document is its own fixed point (law 2), at either level"
);
match (&document, &reparsed) {
(Document::Pattern(a), Document::Pattern(b)) => {
assert_eq!(a, b, "parse(format(ast)) == ast (law 3)");
}
(Document::Score(a), Document::Score(b)) => {
assert_eq!(a, b, "parse(format(ast)) == ast (law 3)");
}
_ => panic!("the canonical text of a document keeps its root"),
}
}
Err(diagnostics) => {
assert!(
!diagnostics.is_empty(),
"a dispatched refusal names at least one diagnostic"
);
for d in &diagnostics {
assert_diagnostic(d, len);
}
}
}
});
3 changes: 3 additions & 0 deletions swang/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

mod ast;
mod diagnostic;
mod document;
mod format;
mod header;
mod lexer;
Expand All @@ -69,9 +70,11 @@ pub use ast::v1::{
MapRhythm, PatternDef, Program, Prune, StrategyName, StrategyPolicy, StringLiteral, Unit,
};
pub use diagnostic::Diagnostic;
pub use document::{format_document, parse_document, parse_document_with_source_map, Document};
pub use format::v1::format;
pub use header::{header_level, LANGUAGE_LEVEL};
pub use parser::v1::{parse, parse_with_source_map};
pub use parser::v2::ExactScore;
pub use source_map::{AstId, FieldKind, FieldRef, Parsed, SourceMap};
pub use span::Span;
#[cfg(test)]
Expand Down
32 changes: 24 additions & 8 deletions swang/src/syntax/ast/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::fmt;

use griff_pattern::{DensityBps, Traversal};

use crate::syntax::header::LANGUAGE_LEVEL;
use crate::TailPolicy;

/// Why an AST value refused to exist.
Expand Down Expand Up @@ -36,7 +35,10 @@ pub enum AstError {
},
/// A unit part is zero — no note value has a zero side.
ZeroUnitPart,
/// Level zero, or newer than [`LANGUAGE_LEVEL`].
/// Any level but 1 — this is the level-1 AST, and it spells one level
/// (spec §5.4). Deliberately not the build-wide range that
/// [`LANGUAGE_LEVEL`](crate::syntax::LANGUAGE_LEVEL) reports: a build
/// understanding a level says nothing about which tree carries it.
UnsupportedLevel {
/// The rejected level.
level: u32,
Expand All @@ -60,26 +62,40 @@ impl fmt::Display for AstError {
Self::ZeroUnitPart => write!(f, "a unit part is zero"),
Self::UnsupportedLevel { level } => write!(
f,
"language level {level} is not supported (1..={LANGUAGE_LEVEL})"
"the level-1 AST spells level 1; it cannot carry level {level}"
),
}
}
}

impl Error for AstError {}

/// A pinned language level, valid by construction: nonzero and at most
/// [`LANGUAGE_LEVEL`].
/// A pinned language level, valid by construction: level 1, the one this
/// AST spells — not every level
/// [`LANGUAGE_LEVEL`](crate::syntax::LANGUAGE_LEVEL) admits, for the reason
/// [`Level::new`] gives.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Level(u32);

/// The one language level this AST spells (spec §5.4: each released level
/// owns its own parser, formatter and tree).
const LEVEL_ONE: u32 = 1;

impl Level {
/// Validates the level against this build's supported range.
/// Validates the level against the one level this AST spells.
///
/// Level 1's AST carries level 1 and nothing else. Validating against
/// `LANGUAGE_LEVEL` instead would mean that raising the constant — which
/// SWG-4A-06 did — silently admits a `Program` whose header says
/// `swang 2` above a `pattern` root: text the formatter emits happily
/// and both entry points refuse, so `parse(format(ast)) == ast` fails
/// for an AST a caller can build from public fields. The mixed value is
/// unconstructible instead of merely unreachable.
///
/// # Errors
/// [`AstError::UnsupportedLevel`] for zero or a newer level.
/// [`AstError::UnsupportedLevel`] for any level but 1.
pub const fn new(level: u32) -> Result<Self, AstError> {
if level == 0 || level > LANGUAGE_LEVEL {
if level != LEVEL_ONE {
return Err(AstError::UnsupportedLevel { level });
Comment on lines +98 to 99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the v1-only level constraint accurately

When a public caller invokes Level::new(2), this correctly returns UnsupportedLevel, but that error's Display now says language level 2 is not supported (1..=2), and the type-level rustdoc still describes every level through LANGUAGE_LEVEL as valid. Since this commit raises LANGUAGE_LEVEL to 2 while restricting this constructor to 1, the resulting public error and API documentation are self-contradictory; describe the constraint as “level 1 only” rather than using the build-wide supported range.

Useful? React with 👍 / 👎.

}
Ok(Self(level))
Expand Down
Loading
Loading