Skip to main content

max / quasi

Start the declare! crate, and make the transition's progress a number Two days of specifying the form produced no implementation: there was no `declare!` anywhere in the workspace, proc-macro or otherwise, so every round could only find more to specify and none could find anything to run. crates/quasi-declare is the walking skeleton. The AST carries only what the first target screen demands and grows one production per conversion rather than ahead of one. `declare!` is a loud compile_error! until the parser lands, because a macro expanding to nothing would let a caller believe a screen had been converted when it had not. scripts/progress.py is the burn-down: declared against remaining, out of the ratified 514, per tree and per file. `--next` is the working queue, smallest unconverted file first, because a whole file is the unit worth taking. Every task in the transition moves that one line. Plan: wiki quasi-phase-1-plan.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-03 16:49 UTC
Signed with PGP, not checked
Commit: b9199a068491ff6d04bbdaddfc1c46e4b91b8383
Parent: 9ad7abb
5 files changed, +328 insertions, -0 deletions
M Cargo.toml +1
@@ -5,6 +5,7 @@
5 5 "crates/quasi-axum",
6 6 "crates/quasi-basics",
7 7 "crates/quasi-bench",
8 + "crates/quasi-declare",
8 9 "crates/quasi-http",
9 10 "crates/quasi-router",
10 11 "crates/quasi-store",
@@ -1,0 +1,21 @@
1 + [package]
2 + name = "quasi-declare"
3 + version = "0.1.0"
4 + description = "The declare! form: a screen description compiled to Rust at build time."
5 + edition.workspace = true
6 + rust-version.workspace = true
7 + authors.workspace = true
8 + repository.workspace = true
9 + license.workspace = true
10 + publish = false
11 +
12 + [lib]
13 + proc-macro = true
14 +
15 + [dependencies]
16 + proc-macro2 = "1"
17 + quote = "1"
18 + syn = { version = "2", features = ["full", "parsing", "printing", "proc-macro"] }
19 +
20 + [lints]
21 + workspace = true
@@ -1,0 +1,145 @@
1 + //! The declared form's syntax tree.
2 + //!
3 + //! This is the grammar of wiki `quasi-declare-form` section 4, narrowed to what
4 + //! a real screen has actually demanded. It grows one production at a time, and
5 + //! every production here exists because a conversion needed it: nothing is
6 + //! specified ahead of a screen that wants it.
7 + //!
8 + //! What the form refuses is as load-bearing as what it admits. There is no
9 + //! expression node, no block in argument position, no closure and no struct
10 + //! literal, because those four are what keep `Node: Eq` intact and keep an
11 + //! argument from reopening into a sub-grammar. A shape that needs one of them
12 + //! calls a supplier function beside the declaration instead.
13 +
14 + // The parser and the emitter land with the first converted screen, so every
15 + // node here is defined before it is read. The allow comes off in that same
16 + // commit; a node still unread after it is a node no screen asked for.
17 + #![allow(dead_code)]
18 +
19 + use proc_macro2::Span;
20 + use syn::{Ident, Type};
21 +
22 + /// One declared shape: a header and the items its body emits.
23 + pub struct Declaration {
24 + pub docs: Vec<String>,
25 + pub vis: Option<syn::Visibility>,
26 + pub name: Ident,
27 + pub params: Vec<Param>,
28 + pub returns: Type,
29 + pub items: Vec<Item>,
30 + }
31 +
32 + pub struct Param {
33 + pub name: Ident,
34 + pub ty: Type,
35 + }
36 +
37 + pub enum Item {
38 + /// `let name = <source>;`
39 + Bind { name: Ident, source: Source },
40 + /// Anything that puts something into the enclosing container.
41 + Emit(Emission),
42 + }
43 +
44 + pub enum Source {
45 + /// A string literal, which may carry `{hole}` interpolations.
46 + Str(Interpolated),
47 + /// A hole: one eager evaluation whose owned result lands in a field.
48 + Hole(Hole),
49 + /// `given <hole> { <literal> -> <source>, .. }` in value position.
50 + ///
51 + /// This is the production the record's amendment 9 was reaching for and
52 + /// missed: it put `dispatch` on the statement side, where an arm is an
53 + /// emission and no emission is a value. Here a value dispatch is its own
54 + /// node whose arms are sources, so it produces a value by construction and
55 + /// cannot admit a block.
56 + Choose {
57 + scrutinee: Hole,
58 + arms: Vec<(Pattern, Source)>,
59 + otherwise: Box<Source>,
60 + },
61 + }
62 +
63 + /// A pattern in value-dispatch position. Literals only: a binding pattern would
64 + /// need a scope, and a scope is how an arm becomes a block.
65 + pub enum Pattern {
66 + Int(i64),
67 + Str(String),
68 + Bool(bool),
69 + /// `Enum::Variant`, emitted verbatim so rustc judges exhaustiveness.
70 + Path(syn::Path),
71 + }
72 +
73 + /// A string with `{hole}` interpolations, compiled to a literal push or a
74 + /// `format!` depending on whether it has any.
75 + pub struct Interpolated {
76 + pub parts: Vec<StrPart>,
77 + pub span: Span,
78 + }
79 +
80 + pub enum StrPart {
81 + Lit(String),
82 + Hole(Hole),
83 + }
84 +
85 + /// One evaluation: a path, optionally called, then field and method steps.
86 + ///
87 + /// A hole is deliberately not an expression. It cannot contain an operator, a
88 + /// closure, a turbofish, an index or a block, so the macro can place its result
89 + /// in a field without reasoning about evaluation order.
90 + pub struct Hole {
91 + pub root: HoleRoot,
92 + pub steps: Vec<Step>,
93 + pub span: Span,
94 + }
95 +
96 + pub enum HoleRoot {
97 + /// A bare lowercase ident: a binding, innermost first.
98 + Binding(Ident),
99 + /// A `::`-qualified or uppercase-initial path: a Rust path, binding nothing.
100 + Path(syn::Path),
101 + /// A module function called at the root of a hole.
102 + Call { path: syn::Path, args: Vec<Arg> },
103 + }
104 +
105 + pub enum Step {
106 + Field(Ident),
107 + Method { name: Ident, args: Vec<Arg> },
108 + }
109 +
110 + pub enum Arg {
111 + Str(Interpolated),
112 + Hole(Hole),
113 + Int(i64),
114 + Bool(bool),
115 + /// `&<hole>`. Nothing borrows implicitly, so the ampersand is written where
116 + /// the code writes it.
117 + Borrow(Box<Arg>),
118 + }
119 +
120 + /// Everything that emits into the enclosing container.
121 + pub enum Emission {
122 + /// `text <arg>;`
123 + Text(Arg),
124 + /// `link <arg> to <action>;`
125 + Link { text: Arg, action: Action },
126 + /// `region <arg> as <kind> { .. }`
127 + Region {
128 + name: Arg,
129 + kind: Ident,
130 + body: Vec<Item>,
131 + },
132 + /// `across <fallback> { .. }`
133 + Across { fallback: Ident, body: Vec<Item> },
134 + /// `beside <priority> <emission>`
135 + Beside {
136 + priority: Ident,
137 + inner: Box<Emission>,
138 + },
139 + }
140 +
141 + pub struct Action {
142 + pub verb: Ident,
143 + pub target: Option<Arg>,
144 + pub modifiers: Vec<Ident>,
145 + }
@@ -1,0 +1,55 @@
1 + //! `declare!`: a screen description, compiled to Rust at build time.
2 + //!
3 + //! The description IS the declaration. What a shape function used to build at
4 + //! request time, this builds once at compile time, so what is left in the
5 + //! binary is literals and holes. Design: wiki `quasi-declare-form`.
6 + //!
7 + //! # How this crate grows
8 + //!
9 + //! One production per conversion. A production exists here because a real
10 + //! screen demanded it, and the screen that demanded it is named in the commit
11 + //! that added it. Nothing is specified ahead of a screen that wants it, because
12 + //! that is what produced a form with twelve amendments and no implementation.
13 + //!
14 + //! When a conversion hits something the form cannot say, there are three
15 + //! outcomes and no fourth: use the remedy the deferred table names, add the
16 + //! production, or refuse it and rewrite the Rust. Adding the production is
17 + //! ordinary and expected. What is never acceptable is an escape hatch that
18 + //! admits an expression, a block in argument position, a closure or a struct
19 + //! literal, because those four are what keep `Node: Eq` intact.
20 +
21 + mod ast;
22 +
23 + use proc_macro::TokenStream;
24 +
25 + /// Declare one shape.
26 + ///
27 + /// ```ignore
28 + /// declare! {
29 + /// /// The strip over the table.
30 + /// shape header(view: &View) -> Node;
31 + ///
32 + /// let base = "/git/{view.owner}/{view.repo}";
33 + /// region "git-file-header" as Group {
34 + /// across Wrap {
35 + /// beside Secondary text lines;
36 + /// beside Essential link "Source" to get "{base}/tree/{view.file_path}" navigating;
37 + /// }
38 + /// }
39 + /// }
40 + /// ```
41 + #[proc_macro]
42 + pub fn declare(input: TokenStream) -> TokenStream {
43 + let _ = input;
44 + // The parser and the emitter land with the first converted screen, MNW
45 + // `git_blame::header`. Until then this is a loud failure rather than a
46 + // quiet no-op: a macro that expands to nothing would let a caller believe
47 + // a screen had been converted when it had not.
48 + quote::quote! {
49 + compile_error!(
50 + "quasi-declare: the parser has not landed yet. \
51 + Track it on GoingsOn, and do not convert a screen against this."
52 + );
53 + }
54 + .into()
55 + }
@@ -1,0 +1,106 @@
1 + #!/usr/bin/env python3
2 + """The declaration transition's burn-down, as one number.
3 +
4 + The transition converts hand-written shape functions into `declare!` blocks.
5 + `population.py` counts what is still hand-written, so as conversion proceeds its
6 + number falls; this counts the other side and prints both, per tree and per file.
7 +
8 + declared 12, remaining 502 of 514 (2.3%)
9 +
10 + That line is the progress of the whole programme. Every task in the transition
11 + should move it, and a task that cannot say how much it moves it is not shaped
12 + right.
13 +
14 + python3 scripts/progress.py # the burn-down
15 + python3 scripts/progress.py --files # per file, converted files hidden
16 + python3 scripts/progress.py --next # the smallest unconverted files first
17 +
18 + `--next` is the working queue: converting a whole small file is worth more than
19 + converting scattered functions, because a file that is fully declared stops
20 + needing its imports and its helpers can move with it.
21 + """
22 +
23 + from __future__ import annotations
24 +
25 + import argparse
26 + import importlib.util
27 + import re
28 + from pathlib import Path
29 +
30 + ROOT = Path.home() / "Code"
31 + HERE = Path(__file__).resolve().parent
32 +
33 + spec = importlib.util.spec_from_file_location("population", HERE / "population.py")
34 + pop = importlib.util.module_from_spec(spec)
35 + spec.loader.exec_module(pop)
36 +
37 + TREES = {
38 + "MNW/server/src/quasi": "mnw",
39 + "Apps/goingson/src-tauri/src/quasi": "goingson",
40 + "Apps/audiofiles/crates/audiofiles-browser/src/quasi": "audiofiles",
41 + }
42 +
43 + # `declare! {` or `quasi_declare::declare! {`, one per declared shape.
44 + DECLARED = re.compile(r"\b(?:quasi_declare\s*::\s*)?declare\s*!\s*[{(]")
45 +
46 + # The ratified starting point, so the percentage has a fixed denominator even
47 + # once conversion has moved the population count. Measured 2026-09-01 and
48 + # reproduced by population.py.
49 + BASELINE = {"mnw": 173, "goingson": 184, "audiofiles": 157}
50 +
51 +
52 + def scan():
53 + per_file = {}
54 + for rel in TREES:
55 + for f in sorted((ROOT / rel).rglob("*.rs")):
56 + parts = f.relative_to(ROOT).parts
57 + if f.name in ("tests.rs", "parity.rs") or "tests" in parts:
58 + continue
59 + raw = f.read_text(encoding="utf-8", errors="replace")
60 + blanked = pop.strip_cfg_test(pop.blank_noncode(raw))
61 + key = str(f.relative_to(ROOT))
62 + hand = pop.shapes_in(f)
63 + per_file[key] = {
64 + "tree": TREES[rel],
65 + "hand": len(hand),
66 + "declared": len(DECLARED.findall(blanked)),
67 + "lines": sum(h.get("lines", 0) for h in hand),
68 + }
69 + return per_file
70 +
71 +
72 + def main() -> int:
73 + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
74 + ap.add_argument("--files", action="store_true", help="per file, fully converted files hidden")
75 + ap.add_argument("--next", action="store_true", help="the smallest unconverted files first")
76 + args = ap.parse_args()
77 +
78 + per_file = scan()
79 + total_base = sum(BASELINE.values())
80 + declared = sum(v["declared"] for v in per_file.values())
81 + remaining = sum(v["hand"] for v in per_file.values())
82 + pct = 100.0 * declared / total_base if total_base else 0.0
83 +
84 + for tree, base in BASELINE.items():
85 + d = sum(v["declared"] for v in per_file.values() if v["tree"] == tree)
86 + h = sum(v["hand"] for v in per_file.values() if v["tree"] == tree)
87 + print(f" {tree:11} declared {d:4}, remaining {h:4} of {base}")
88 + print(f"\ndeclared {declared}, remaining {remaining} of {total_base} ({pct:.1f}%)")
89 +
90 + if args.next:
91 + todo = [(k, v) for k, v in per_file.items() if v["hand"]]
92 + todo.sort(key=lambda kv: (kv[1]["lines"], kv[1]["hand"]))
93 + print("\nSmallest unconverted files first. A whole file is the unit worth taking.\n")
94 + for k, v in todo[:20]:
95 + print(f" {v['hand']:3} shapes {v['lines']:5}L {k}")
96 +
97 + if args.files:
98 + print()
99 + for k, v in sorted(per_file.items()):
100 + if v["hand"]:
101 + print(f" {v['declared']:3}/{v['declared'] + v['hand']:3} {k}")
102 + return 0
103 +
104 +
105 + if __name__ == "__main__":
106 + raise SystemExit(main())