Skip to content
Closed
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
18 changes: 18 additions & 0 deletions dsl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ proc-macro2 = "1.0.86"
convert_case = "0.6.0"
indexmap = "2"
serde = { version = "1", features = ["derive"], optional = true }
# Only ever reached through the `json` feature, which only the `worktable-schemas` binary
# needs. Kept out of `serde` so that turning the IR into data does not also pull a parser in.
serde_json = { version = "1", optional = true }

[dev-dependencies]
# The integration test builds as its own crate, which is what makes it evidence
Expand Down Expand Up @@ -44,3 +47,18 @@ spans = ["proc-macro2/span-locations"]
# diagram, next to whatever it does enforce. Kept here because it is written
# and tested, behind a flag so that turning it on is a decision.
uml = []
# The `worktable-schemas` binary, which dumps every declaration in a tree as JSON so an emitter
# in another language can be checked against real schemas. Separate from `serde` because
# serialising the IR and shipping a JSON writer are different costs to ask of a dependent.
json = ["serde", "dep:serde_json"]

[[bin]]
name = "worktable-schemas"
path = "src/bin/worktable-schemas.rs"
required-features = ["json"]

[[bin]]
# Needs nothing beyond the default build: it is the binary a cross-implementation test runs on
# every case, so anything it required would be a thing that test could be missing.
name = "worktable-parse"
path = "src/bin/worktable-parse.rs"
54 changes: 54 additions & 0 deletions dsl/src/bin/worktable-parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! `worktable-parse` — read a declaration on stdin, write its canonical text to stdout.
//!
//! The input is the macro **body**: `name: Foo, columns: { .. }`, without `worktable!` or the
//! surrounding braces, which is what [`worktable_dsl::Schema::parse`] accepts and what
//! [`worktable_dsl::Schema::to_dsl`] produces.
//!
//! It exists so that an emitter written in another language can be checked against this one.
//! Two implementations of a language drift unless something compares them, and the comparison
//! has to be byte-exact: they can agree on every meaning and still disagree on every character
//! a person reads.
//!
//! It runs [`worktable_dsl::check`] rather than `Schema::parse`, and that is the whole point.
//! `parse` answers "is this a declaration"; `check` answers "would the macro accept it", which
//! is the question an emitter has to get right. `page_size: 4096` beside `persist: true` parses
//! perfectly and the macro refuses it, so a parse-only binary would report green for output
//! that does not compile.

use std::io::{Read, Write};

fn main() {
let mut source = String::new();
if std::io::stdin().read_to_string(&mut source).is_err() {
eprintln!("worktable-parse: could not read stdin");
std::process::exit(2);
}

let checked = worktable_dsl::check(&source);

for d in &checked.diagnostics {
let stage = match d.stage {
worktable_dsl::Stage::Grammar => "grammar",
worktable_dsl::Stage::Rules => "rules",
};
match d.span {
// Byte ranges need the `spans` feature. Absence of a location is never absence of
// a problem, so the message is printed either way.
Some(s) => eprintln!("{stage} [{}..{}]: {}", s.start, s.end, d.message),
None => eprintln!("{stage}: {}", d.message),
}
}

let Some(schema) = checked.schema else {
std::process::exit(1);
};
if !checked.diagnostics.is_empty() {
// A rule violation still yields a schema — an editor has to draw it so somebody can
// fix it — but it is not text the macro would accept, so it is not success.
std::process::exit(1);
}

if std::io::stdout().write_all(schema.to_dsl().as_bytes()).is_err() {
std::process::exit(2);
}
}
72 changes: 72 additions & 0 deletions dsl/src/bin/worktable-schemas.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! `worktable-schemas` — every `worktable!` declaration under a directory, as JSON.
//!
//! Each entry carries the schema and the canonical text this crate emits for it, so a consumer
//! written in another language can emit from the same model and compare bytes without needing
//! to reproduce the scan or the parse.
//!
//! It exists to make the cross-implementation check run against *real* declarations rather than
//! against the handful anybody thinks to invent. A hand-written corpus tests the cases its
//! author already understood; this repository's own tables are the ones people actually wrote,
//! and they are where an emitter's unexamined assumption shows up.
//!
//! Requires the `serde` feature, which is off by default because `worktable_dsl` is compiled
//! for the host as part of `worktable_codegen` before anything else in a dependent's build.

use std::io::Write;
use std::path::Path;

fn main() {
let root = std::env::args().nth(1).unwrap_or_else(|| ".".to_string());

let mut entries: Vec<String> = Vec::new();
let mut templates = 0usize;
let mut rejected = 0usize;
walk(Path::new(&root), &mut entries, &mut templates, &mut rejected);

// Sorted, so the output is a function of the tree's contents rather than of the order a
// directory happened to be read in. A consumer diffing two runs should see only real change.
entries.sort();

let body = entries.join(",\n ");
let out = format!(
"{{\n \"templates\": {templates},\n \"rejected\": {rejected},\n \"schemas\": [\n {body}\n ]\n}}\n"
);
if std::io::stdout().write_all(out.as_bytes()).is_err() {
std::process::exit(2);
}
}

fn walk(dir: &Path, entries: &mut Vec<String>, templates: &mut usize, rejected: &mut usize) {
let Ok(read) = std::fs::read_dir(dir) else { return };
for entry in read.flatten() {
let path = entry.path();
let name = entry.file_name();
let name = name.to_string_lossy();
if path.is_dir() {
// `target` is build output and would multiply the scan by every vendored crate.
if name == "target" || name == ".git" || name == "node_modules" {
continue;
}
walk(&path, entries, templates, rejected);
continue;
}
if path.extension().is_none_or(|e| e != "rs") {
continue;
}
let Ok(source) = std::fs::read_to_string(&path) else { continue };
if !source.contains("worktable!") {
continue;
}
let Ok(found) = worktable_dsl::declarations_in_source(&source) else { continue };
*templates += found.templates.len();
*rejected += found.rejected.len();
for schema in found.schemas {
let dsl = serde_json::to_string(&schema.to_dsl()).expect("a string serialises");
let model = serde_json::to_string(&schema).expect("the schema serialises");
entries.push(format!(
"{{ \"file\": {}, \"dsl\": {dsl}, \"schema\": {model} }}",
serde_json::to_string(&path.display().to_string()).expect("a path serialises")
));
}
}
}