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: 3 additions & 3 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ jobs:
STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification
run: cargo +stable test -p stack-formatter --features conformance --test conformance --locked

- name: Run canonical layout snapshot
- name: Run canonical scene suite
env:
STACK_SPECIFICATION_DIR: ${{ github.workspace }}/.stack-specification
run: cargo +stable test -p stack-engine --features conformance canonical_complete_semantics_matches_snapshot --locked
run: cargo +stable test -p stack-engine --features conformance --locked

- name: Build pure engine for WebAssembly
run: |
Expand All @@ -64,7 +64,7 @@ jobs:
- name: Verify native and WebAssembly geometry parity
env:
CARGO_TARGET_WASM32_WASIP1_RUNNER: wasmtime
run: cargo +stable test -p stack-engine --lib --target wasm32-wasip1 geometry_matches_cross_target_numeric_fixture --locked
run: cargo +stable test -p stack-engine --lib --target wasm32-wasip1 cross_target_numeric_fixture --locked

- name: Run Clippy
run: cargo +stable clippy --workspace --all-targets --all-features --locked -- -D warnings
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

`stack-sh/engine` is the pure Rust execution engine for Stack architecture diagrams.

The workspace now provides canonical Stack source formatting, the pure `stack-engine` operation facade, and deterministic theme-aware scene layout. SVG rendering and WebAssembly adapters remain planned work.
The workspace now provides canonical Stack source formatting, the pure `stack-engine` operation facade, deterministic theme-aware scene layout, and orthogonal edge routing. SVG rendering and WebAssembly adapters remain planned work.

## Planned workspace

- `stack-engine`: implemented operation/output boundary, theme resolution, deterministic scene layout, and validation beyond the compiler stage, plus planned standalone SVG rendering;
- `stack-engine`: implemented operation/output boundary, theme resolution, deterministic scene layout, edge routing, and validation beyond the compiler stage, plus planned standalone SVG rendering;
- `stack-formatter`: comment-preserving canonical formatting for Stack source files (implemented);
- a WebAssembly adapter exposing the same pure operations to browser consumers.

Expand All @@ -25,23 +25,24 @@ The workspace uses Rust 2024 with Rust 1.85 as its minimum supported version. Ru
```sh
cargo test --workspace
STACK_SPECIFICATION_DIR=../specification cargo test -p stack-formatter --features conformance --test conformance
STACK_SPECIFICATION_DIR=../specification cargo test -p stack-engine --features conformance canonical_complete_semantics_matches_snapshot
STACK_SPECIFICATION_DIR=../specification cargo test -p stack-engine --features conformance
cargo build -p stack-engine --target wasm32-unknown-unknown
CARGO_TARGET_WASM32_WASIP1_RUNNER=wasmtime cargo test -p stack-engine --lib --target wasm32-wasip1 geometry_matches_cross_target_numeric_fixture
CARGO_TARGET_WASM32_WASIP1_RUNNER=wasmtime cargo test -p stack-engine --lib --target wasm32-wasip1 cross_target_numeric_fixture
cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo doc --workspace --no-deps
```

`stack-formatter` is pure and accepts source bytes or UTF-8 text. Lexical and syntax errors return diagnostics without formatted output. Syntactically valid source remains formattable when semantic diagnostics exist.

`stack-engine` exposes byte-oriented `format`, `check`, and reserved `render` methods through an engine bound to the embedded or a caller-provided validated catalog. Every normal output carries engine, authored language, theme catalog version, and theme catalog revision metadata. User-source failures stay in ordered portable diagnostics. Invalid provided catalogs, invalid normalized containment, and unavailable pipeline stages use a separate operational-error channel. Checks and compiler-valid render attempts now resolve the requested theme and validate a deterministic integer scene; rendering remains unavailable until SVG integration lands. CI executes one exact numeric geometry fixture in both the native suite and a WASI build.
`stack-engine` exposes byte-oriented `format`, `check`, and reserved `render` methods through an engine bound to the embedded or a caller-provided validated catalog. Every normal output carries engine, authored language, theme catalog version, and theme catalog revision metadata. User-source failures stay in ordered portable diagnostics. Invalid provided catalogs, invalid normalized containment, routing failure, and unavailable pipeline stages use a separate operational-error channel. Checks and compiler-valid render attempts resolve the requested theme, validate deterministic integer geometry, and route ordered edges outside node interiors. An unsatisfied authored order hint produces `STK4001` at its source-map range; a satisfied hint does not. Rendering remains unavailable until SVG integration lands. CI executes one exact numeric geometry fixture in both the native suite and a WASI build.

## Architecture

- [`docs/decisions/0001-build-the-formatter-from-compiler-models.md`](./docs/decisions/0001-build-the-formatter-from-compiler-models.md)
- [`docs/decisions/0002-use-a-pure-versioned-engine-facade.md`](./docs/decisions/0002-use-a-pure-versioned-engine-facade.md)
- [`docs/decisions/0003-use-integer-ranked-scene-layout.md`](./docs/decisions/0003-use-integer-ranked-scene-layout.md)
- [`docs/decisions/0004-route-orthogonal-edges-on-a-visibility-grid.md`](./docs/decisions/0004-route-orthogonal-edges-on-a-visibility-grid.md)
- [`docs/dependency-audit.md`](./docs/dependency-audit.md)

## Licensing
Expand Down
138 changes: 129 additions & 9 deletions crates/stack-engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::fmt;

use stack_compiler::diagnostic as compiler_diagnostic;

mod routing;
mod scene;

/// Version of the Rust engine facade.
Expand Down Expand Up @@ -108,14 +109,20 @@ impl<'catalog> Engine<'catalog> {
})
}

/// Runs the currently available compiler stages without producing SVG.
/// Runs compiler, theme, layout, and routing validation without producing SVG.
pub fn check(&self, source: &[u8]) -> OperationResult<CheckOutput> {
let compiled = stack_compiler::compile_bytes(source);
let compiled = stack_compiler::compile_bytes_with_source_map(source);
let mut diagnostics = portable_diagnostics(compiled.diagnostics);
if let Some(diagram) = &compiled.diagram {
self.validate_scene(diagram)?;
let source_map = compiled.source_map.as_ref().ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "compiler omitted the source map for normalized IR",
},
)?;
diagnostics.extend(self.validate_scene(diagram, source_map)?);
}
Ok(CheckOutput {
diagnostics: portable_diagnostics(compiled.diagnostics),
diagnostics,
metadata: self.metadata(declared_language_version(source)),
})
}
Expand All @@ -128,7 +135,7 @@ impl<'catalog> Engine<'catalog> {
/// The latter branch is replaced by deterministic SVG output when the render
/// pipeline lands.
pub fn render(&self, source: &[u8]) -> OperationResult<RenderOutput> {
let compiled = stack_compiler::compile_bytes(source);
let compiled = stack_compiler::compile_bytes_with_source_map(source);
let metadata = self.metadata(declared_language_version(source));
if compiled.diagram.is_none() {
return Ok(RenderOutput {
Expand All @@ -139,7 +146,12 @@ impl<'catalog> Engine<'catalog> {
}

if let Some(diagram) = &compiled.diagram {
self.validate_scene(diagram)?;
let source_map = compiled.source_map.as_ref().ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "compiler omitted the source map for normalized IR",
},
)?;
self.validate_scene(diagram, source_map)?;
}

Err(OperationalError::PipelineUnavailable {
Expand All @@ -156,7 +168,11 @@ impl<'catalog> Engine<'catalog> {
}
}

fn validate_scene(&self, diagram: &stack_compiler::ir::Diagram) -> OperationResult<()> {
fn validate_scene(
&self,
diagram: &stack_compiler::ir::Diagram,
source_map: &stack_compiler::source_map::SourceMap,
) -> OperationResult<Vec<Diagnostic>> {
let scene = scene::layout(diagram, self.catalog).map_err(|error| {
OperationalError::InvalidIntermediateRepresentation {
reason: error.reason(),
Expand All @@ -167,10 +183,41 @@ impl<'catalog> Engine<'catalog> {
reason: "layout produced invalid containment or overlap geometry",
});
}
Ok(())
scene
.unsatisfied_orders
.iter()
.map(|scope| order_diagnostic(scope, source_map))
.collect()
}
}

fn order_diagnostic(
scope: &scene::SceneScope,
source_map: &stack_compiler::source_map::SourceMap,
) -> OperationResult<Diagnostic> {
let origin = match scope {
scene::SceneScope::Diagram => source_map.diagram_order(),
scene::SceneScope::Group(identifier) => source_map.group_order(identifier).ok_or(
OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted a normalized group",
},
)?,
};
let span = origin
.span()
.ok_or(OperationalError::InvalidIntermediateRepresentation {
reason: "source map omitted an authored order hint",
})?;
Ok(Diagnostic {
code: "STK4001".to_owned(),
severity: Severity::Warning,
message: "order hint could not be satisfied by deterministic layout".to_owned(),
range: SourceRange::from(span),
help: Some("Adjust the order hint or same-rank constraints.".to_owned()),
related: Vec::new(),
})
}

/// Operation whose execution may produce an operational error.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operation {
Expand Down Expand Up @@ -265,7 +312,7 @@ pub struct FormatOutput {
/// Result of the check operation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckOutput {
/// Compiler and, in later stages, theme and layout diagnostics in deterministic order.
/// Compiler, theme, and layout diagnostics in deterministic order.
pub diagnostics: Vec<Diagnostic>,
/// Versions that identify the exact operation implementation and inputs.
pub metadata: EngineMetadata,
Expand Down Expand Up @@ -403,6 +450,8 @@ impl From<compiler_diagnostic::SourcePosition> for SourcePosition {

#[cfg(test)]
mod tests {
use std::error::Error;

use stack_compiler::diagnostic as compiler_diagnostic;

use super::{
Expand Down Expand Up @@ -482,6 +531,77 @@ mod tests {
}
}

#[test]
fn check_emits_order_warning_at_the_authored_statement() -> Result<(), Box<dyn Error>> {
let source = "stack 1.0 diagram \"Order\" { layout { direction right order [b, a] } node a \"A\" node b \"B\" }";
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(output.diagnostics.len(), 1);
let diagnostic = &output.diagnostics[0];
assert_eq!(diagnostic.code, "STK4001");
assert_eq!(diagnostic.severity, Severity::Warning);
let start = source
.find("order [b, a]")
.ok_or("missing order statement")?;
let end = start + "order [b, a]".len();
assert_eq!(diagnostic.range.start.byte_offset, start as u64);
assert_eq!(diagnostic.range.end.byte_offset, end as u64);
assert_eq!(diagnostic.range.start.line, 1);
assert_eq!(diagnostic.range.start.column, start as u64 + 1);
assert_eq!(diagnostic.range.end.column, end as u64 + 1);
Ok(())
}

#[test]
fn check_omits_order_warning_when_rank_placement_satisfies_it() -> Result<(), Box<dyn Error>> {
let source = b"stack 1.0 diagram \"Order\" { layout { direction right rank same [a, b] order [b, a] } node a \"A\" node b \"B\" }";
let output = Engine::bundled().check(source)?;
assert!(output.diagnostics.is_empty());
Ok(())
}

#[test]
fn group_order_warning_uses_the_group_source_map_entry() -> Result<(), Box<dyn Error>> {
let source = "stack 1.0 diagram \"Group order\" { group pair \"Pair\" { layout { direction down order [b, a] } node a \"A\" node b \"B\" } }";
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(output, Engine::bundled().check(source.as_bytes())?);
assert_eq!(
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
vec!["STK4001"]
);
let start = source
.find("order [b, a]")
.ok_or("missing order statement")?;
assert_eq!(output.diagnostics[0].range.start.byte_offset, start as u64);
Ok(())
}

#[test]
fn layout_warnings_follow_compiler_warnings() -> Result<(), Box<dyn Error>> {
let mut source = String::from(
"stack 1.0 diagram \"Warnings\" { layout { direction right order [n1, n0] } node hub \"Hub\" ",
);
for index in 0..13 {
source.push_str(&format!(
"node n{index} \"N {index}\" edge hub -> n{index} "
));
}
source.push('}');
let output = Engine::bundled().check(source.as_bytes())?;
assert_eq!(
output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>(),
vec!["STK4002", "STK4001"]
);
Ok(())
}

#[test]
fn invalid_render_input_is_not_an_operational_error() {
let engine = Engine::bundled();
Expand Down
Loading