max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
5 files changed,
+328 insertions,
-0 deletions
| @@ -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()) |