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
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
59 changes: 59 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Install latest stable Rust toolchain
run: rustup toolchain install stable --profile minimal --component clippy,rustfmt,llvm-tools-preview

- name: Check formatting
run: cargo +stable fmt --check

- name: Run tests
run: cargo +stable test

- name: Run Clippy
run: cargo +stable clippy --all-targets --all-features -- -D warnings

- name: Build documentation
env:
RUSTDOCFLAGS: -D warnings
run: cargo +stable doc --no-deps

- name: Install cargo-llvm-cov
uses: taiki-e/install-action@e67fa11c4b9316fa714ddf0abed07a0c3143b95b # v2.87.4
with:
tool: cargo-llvm-cov@0.9.0
fallback: none

- name: Enforce unit-test coverage
run: cargo +stable llvm-cov --lib --all-features --workspace --fail-under-lines 95 --fail-under-functions 95 --fail-under-regions 95

msrv:
name: Minimum supported Rust
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Install Rust 1.85
run: rustup toolchain install 1.85.0 --profile minimal --component clippy

- name: Run tests
run: cargo +1.85.0 test

- name: Run Clippy
run: cargo +1.85.0 clippy --all-targets --all-features -- -D warnings
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Logs
logs
*.log

# Rust build output
/target
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Expand Down
43 changes: 43 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Stack Compiler Agent Guide

## Source of Truth

The canonical language contract is `stack-sh/specification`. Never introduce syntax, defaults, semantics, or `STK` diagnostic meanings that are not defined there.

## Technology

- Rust 2024 edition
- Latest stable Rust and Cargo for development and primary CI
- Rust 1.85 minimum supported version, verified separately in CI
- Standard library only unless an ADR accepts a dependency
- No unsafe Rust

## Commands

- Format: `cargo fmt --check`
- Test: `cargo test`
- Unit-test coverage: `cargo llvm-cov --lib --all-features --workspace --fail-under-lines 95 --fail-under-functions 95 --fail-under-regions 95`
- Lint: `cargo clippy --all-targets --all-features -- -D warnings`
- Documentation: `cargo doc --no-deps`

## Conventions

- Keep parsing, semantic validation, and normalization as separate stages.
- Preserve source spans and duplicate declarations in the AST so validation can report the authored mistake.
- Keep the normalized IR deterministic, renderer-independent, and free of filesystem or network handles.
- Use specification-assigned `STK` codes only for their normative meanings.
- Add focused tests for every diagnostic or language rule implemented.
- Keep line, function, and region coverage at or above 95 percent.
- Keep GitHub Actions on their latest supported major versions; Dependabot checks for updates weekly.
- Do not use panic-producing `unwrap`, `expect`, `panic`, `unreachable`, `todo`, or `unimplemented` macros in any target; package-level Clippy lints enforce this boundary.
- Write repository content, code comments, issues, and pull requests in English.
- Keep temporary implementation plans and task lists outside the repository under `/tmp`.

## Boundaries

- Always run formatting, tests, coverage, Clippy, and documentation checks before delivery.
- Ask before adding a runtime dependency or changing a public representation.
- Do not create a commit unless the user explicitly requests one.
- Never add theme resolution, icon retrieval, layout, SVG rendering, HTTP, authentication, or storage access to this repository.
- Never add temporary plan or todo files to the repository or its ignore rules.
- Never commit credentials, generated build output, or editor-specific state.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "stack-compiler"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
description = "Reference compiler frontend for the Stack diagram language"
repository = "https://github.com/stack-sh/compiler"
readme = "README.md"
keywords = ["stack", "diagram", "dsl", "compiler"]
categories = ["parser-implementations"]

[lib]
path = "src/lib.rs"

[lints.clippy]
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
unreachable = "deny"
unwrap_used = "deny"
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
# compiler
# Stack Compiler

`stack-compiler` is the reference Rust frontend for the [Stack language](https://github.com/stack-sh/specification). It parses Stack source, validates its language semantics, applies specification-defined defaults, and produces a renderer-independent normalized diagram.

The compiler does not resolve themes or icons, calculate layout, render SVG, access the network, or read files. Those concerns belong to downstream libraries and applications.

## Status

Stack 1.0 and this compiler are both under active development. Public Rust APIs may change before the first stable release.

Development and primary CI follow the latest stable Rust and Cargo releases through [`rust-toolchain.toml`](./rust-toolchain.toml). Rust 1.85 remains the minimum supported version and is verified in a separate CI job.

## Pipeline

```text
Stack source
-> lexical analysis
-> syntax AST
-> semantic validation
-> normalized Diagram IR
```

## Commands

| Command | Purpose |
| --- | --- |
| `cargo test` | Run the test suite |
| `cargo llvm-cov --lib --all-features --workspace --fail-under-lines 95 --fail-under-functions 95 --fail-under-regions 95` | Enforce unit-test coverage |
| `cargo fmt --check` | Check Rust formatting |
| `cargo clippy --all-targets --all-features -- -D warnings` | Run the linter |
| `cargo doc --no-deps` | Build API documentation |

Install [`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov) before running the coverage command. CI measures the library unit tests independently and requires line, function, and region coverage to remain at or above 95 percent.

Package-level Clippy lints reject panic-producing `unwrap`, `expect`, `panic`, `unreachable`, `todo`, and `unimplemented` calls in library and test targets.

## Architecture

- [`docs/decisions/0001-build-a-portable-rust-compiler-core.md`](./docs/decisions/0001-build-a-portable-rust-compiler-core.md)
- [`docs/decisions/0002-separate-syntax-ast-from-normalized-ir.md`](./docs/decisions/0002-separate-syntax-ast-from-normalized-ir.md)
- [`docs/decisions/0003-use-a-handwritten-parser.md`](./docs/decisions/0003-use-a-handwritten-parser.md)
- [`docs/specs/compiler-frontend.md`](./docs/specs/compiler-frontend.md)

## License

Apache-2.0
61 changes: 61 additions & 0 deletions docs/decisions/0001-build-a-portable-rust-compiler-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# ADR-0001: Build a Portable Rust Compiler Core

## Status

Accepted

## Date

2026-09-02

## Context

Stack source must produce the same meaning in a browser, a native CLI, and a hosted rendering service. The initial hosted design may use Cloudflare Workers, but the language implementation must not depend on one HTTP runtime, storage provider, or commercial product boundary.

The compiler is small and CPU-bound. It does not need theme assets, network access, a filesystem, or renderer state. Rust can produce native libraries and binaries for CLI use and WebAssembly modules for browser or Worker use from the same core implementation.

## Decision

Implement the Stack compiler frontend as a pure Rust library.

This repository owns the pipeline from decoded Stack source through normalized diagram IR:

1. lexical analysis;
2. syntax parsing;
3. identifier and default resolution;
4. semantic and complexity validation;
5. normalized IR construction.

The core API accepts source bytes or text and returns typed values and structured diagnostics. It must be deterministic and must not perform network, filesystem, clock, random, environment, or platform-specific operations.

Native Rust is the first supported target. WebAssembly bindings and a native CLI may wrap the same core in later changes, but target-specific adapters must remain outside the compiler stages.

Theme and icon resolution, layout, SVG rendering, HTTP handling, authentication, caching, and storage are explicitly outside this repository.

## Alternatives Considered

### TypeScript-only compiler

- Pros: Direct integration with browsers and Cloudflare Workers; shared types with web applications.
- Cons: Makes a native CLI less direct and ties the reference implementation more closely to JavaScript runtimes.
- Rejected: Rust provides the desired native and WebAssembly portability while a thin TypeScript host can still call the compiled module.

### Go compiler in a container service

- Pros: Straightforward native service deployment and good server concurrency.
- Cons: Browser execution and Worker integration are less direct; adopting a container as the language boundary couples local rendering to a hosted API.
- Rejected: A portable local engine is a primary product property, not only a deployment optimization.

### Hosted API as the only compiler interface

- Pros: One centrally deployed implementation and simple client code.
- Cons: Requires network access, adds operating cost and latency, and prevents private offline rendering.
- Rejected: Hosted rendering may be offered later as a managed convenience, but it is not required for language correctness.

## Consequences

- Browser, CLI, and hosted products can share one language implementation.
- The compiler can be tested without infrastructure or external assets.
- Rust-to-WebAssembly interface design and binary size require explicit verification later.
- Downstream repositories consume normalized IR rather than syntax-specific AST details.
- Commercial services must add value through hosting, collaboration, private assets, governance, or support rather than exclusive access to language semantics.
55 changes: 55 additions & 0 deletions docs/decisions/0002-separate-syntax-ast-from-normalized-ir.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# ADR-0002: Separate the Syntax AST from Normalized IR

## Status

Accepted

## Date

2026-09-02

## Context

A parser must represent what an author wrote, including omitted properties, source order, duplicate properties, unresolved identifiers, and precise source locations. Layout and rendering code should not need to interpret those syntax choices or independently apply Stack defaults.

Using the syntax AST as the cross-repository interface would expose grammar details to every downstream consumer. It would also force each consumer to repeat semantic validation and normalization.

## Decision

Expose two distinct representations:

- The syntax AST mirrors Stack declarations and properties. It preserves source spans, authored order, omissions, and duplicates needed for diagnostics.
- The normalized IR represents a semantically valid diagram. It applies specification-defined defaults, resolves structural membership, separates nodes, groups, and edges, and uses typed enums for closed semantic values.

Semantic validation is a distinct pass between the two representations. The compiler produces normalized IR only when lexical, syntax, and semantic errors are absent. Warnings do not prevent IR construction.

The normalized IR must be deterministic, serializable without runtime handles, and independent of themes, layout engines, and renderers. Renderer-selected defaults, resolved theme data, icon SVG, coordinates, and text metrics do not belong in this IR.

Source positions use one-based line and column numbers in diagnostics. Internally, spans also retain zero-based UTF-8 byte offsets. Range ends are exclusive.

## Alternatives Considered

### Let layout consume the AST

- Pros: No additional representation or lowering pass.
- Cons: Couples layout to grammar and duplicates defaulting, reference resolution, and validation across consumers.
- Rejected: Syntax is not a stable renderer contract.

### Normalize the AST in place

- Pros: Fewer data types and allocations.
- Cons: Loses whether a value was authored or defaulted and prevents precise duplicate-property diagnostics.
- Rejected: Diagnostic tooling and downstream rendering need different information.

### Include resolved theme and icon assets in compiler IR

- Pros: One object contains everything needed by layout.
- Cons: Makes compilation depend on a catalog version and external assets, and prevents pure offline language validation.
- Rejected: Theme resolution creates a later visual representation owned by downstream code.

## Consequences

- AST types may evolve with grammar additions, while normalized IR is the intentional downstream boundary.
- Parser and validator tests can assert authored syntax independently from normalized meaning.
- The compiler performs a small allocation cost to lower from AST to IR.
- Future formatter work may add a concrete syntax or token representation for comment preservation without placing trivia in normalized IR.
46 changes: 46 additions & 0 deletions docs/decisions/0003-use-a-handwritten-parser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# ADR-0003: Use a Handwritten Parser

## Status

Accepted

## Date

2026-09-02

## Context

Stack 1.0 has a small, intentionally constrained grammar with contextual keywords. Diagnostics require exact source spans and stable Stack-specific error codes. The parser must preserve duplicate declarations and properties for a later semantic validation pass.

A parser generator could reduce some grammar code, but it would add a runtime dependency and make error recovery, contextual identifiers, and diagnostic mapping depend on generator behavior.

## Decision

Use a handwritten lexer and recursive-descent parser for Stack 1.x.

The lexer produces spanned tokens and handles comments, strings, escapes, punctuation, operators, and UTF-8 position tracking. Keywords remain ordinary word tokens and are interpreted by the parser according to grammatical context.

The parser follows the canonical EBNF directly. It rejects unknown declarations, properties, values, and operators. Initial syntax recovery may stop after the first lexical or syntax error because multi-error parser recovery is optional in Stack 1.0. Semantic validation must still collect independent errors in one pass.

No parser dependency is added initially. A future ADR may replace this parser if grammar growth or recovery requirements make the handwritten implementation materially harder to maintain.

## Alternatives Considered

### Parser generator

- Pros: Declarative grammar and generated parsing machinery.
- Cons: Additional dependency, less direct control over Stack diagnostics, and generator-specific recovery behavior.
- Rejected for the initial grammar: The language is small enough for direct implementation.

### Parser combinator library

- Pros: Reusable primitives and compact parser code.
- Cons: Additional dependency and error composition that may not align with normative diagnostic codes.
- Rejected for the initial grammar: Standard-library code is sufficient.

## Consequences

- Parser code remains explicit and easy to compare with the EBNF.
- Source range and diagnostic behavior stay under project control.
- Grammar changes require corresponding manual parser and test updates.
- The project must keep parsing functions small and structurally aligned with specification productions.
Loading