Skip to main content

blue_lang_syntax/
forms.rs

1//! The surface forms, one row each: how a form is written, the tatara-lisp
2//! tree it lowers to, and one line on what it means.
3//!
4//! This is the table `blue reference` prints as the grammar card. A reference
5//! written by hand next to a parser drifts the first time the parser changes;
6//! here every row's `example` is parsed by the real parser in
7//! `every_form_lowers_as_its_row_says`, and the tree must be exactly
8//! `lowers_to`. So a row that stops being true is a red build, not a stale
9//! document.
10//!
11//! Operators live in [`crate::parse::INFIX`] and reserved words in
12//! [`crate::parse::SURFACE_KEYWORDS`] / [`crate::parse::BLOCK_KEYWORDS`],
13//! each with its own line; this table holds the forms that are neither.
14
15/// One surface form.
16#[derive(Clone, Copy, Debug)]
17pub struct Form {
18    /// The form as an author writes it. Parsed by the gate.
19    pub example: &'static str,
20    /// The tree `example` parses to, as `Sexp`'s `Display` prints it.
21    pub lowers_to: &'static str,
22    /// One line for a reader.
23    pub doc: &'static str,
24}
25
26/// Every surface form that is not an operator or a keyword.
27pub const FORMS: &[Form] = &[
28    Form {
29        example: "42",
30        lowers_to: "42",
31        doc: "An Int. Underscores may separate digits: 1_000.",
32    },
33    Form {
34        example: "3.5",
35        lowers_to: "3.5",
36        doc: "A Float.",
37    },
38    Form {
39        example: "\"text\"",
40        lowers_to: "\"text\"",
41        doc: "A string. Escapes: \\n, \\t, \\\", \\\\, \\u{1b}.",
42    },
43    Form {
44        example: "\"n = #{n}!\"",
45        lowers_to: "(concat (concat \"n = \" n) \"!\")",
46        doc: "Interpolation: any expression inside #{…}, rendered with to_s. A literal that must contain #{ is built with concat(\"#\", \"{\").",
47    },
48    Form {
49        example: ":done",
50        lowers_to: ":done",
51        doc: "A keyword: a name as a value. It may not end in ? or !.",
52    },
53    Form {
54        example: "[1, \"a\"]",
55        lowers_to: "(list 1 \"a\")",
56        doc: "A list. Indexing is nth(i, xs); blue has no xs[i].",
57    },
58    Form {
59        example: "{a: 1, \"k\" => 2}",
60        lowers_to: "(hash-map :a 1 \"k\" 2)",
61        doc: "A map. `a:` is the keyword key :a; `=>` takes any key. Read with get(m, k), extend with assoc(m, k, v).",
62    },
63    Form {
64        example: "x = 5",
65        lowers_to: "(define x 5)",
66        doc: "A binding, only as a statement. There is no `let`; a binding lasts to the end of its body.",
67    },
68    Form {
69        example: "f(x, y)",
70        lowers_to: "(f x y)",
71        doc: "A call. This is the one form the formatter writes.",
72    },
73    Form {
74        example: "x.f(y)",
75        lowers_to: "(f x y)",
76        doc: "A method-style call: the receiver becomes the first argument. Formatted as f(x, y).",
77    },
78    Form {
79        example: "xs |> f(a)",
80        lowers_to: "(f xs a)",
81        doc: "A pipe: the value becomes the FIRST argument, so it suits data-first words. map(f, xs) is function-first: xs |> map(f) is map(xs, f), an error.",
82    },
83    Form {
84        example: "!x",
85        lowers_to: "(not x)",
86        doc: "Negation. `not(x)` is the same tree.",
87    },
88    Form {
89        example: "-x",
90        lowers_to: "(- 0 x)",
91        doc: "Unary minus. The formatter writes 0 - x as -x.",
92    },
93    Form {
94        example: "use(\"retsu\")",
95        lowers_to: "(use \"retsu\")",
96        doc: "Import a bidama by name, found on BLUE_PATH. Every definition it makes joins one flat namespace.",
97    },
98    Form {
99        example: "throw(error(:parse, \"bad row\"))",
100        lowers_to: "(throw (error :parse \"bad row\"))",
101        doc: "Raise. error(kind, message) alone only builds the value; throw raises it, and uncaught it fails the run.",
102    },
103    Form {
104        example: "try(risky(), catch(e(), fallback(e)))",
105        lowers_to: "(try (risky) (catch (e) (fallback e)))",
106        doc: "Catch a throw. The binding is written as a call, e(). error?(e) is true inside; the kind and message are not readable, so return refusals as data when a caller must tell them apart.",
107    },
108];
109
110#[cfg(test)]
111mod tests {
112    use super::FORMS;
113    use crate::parse::{keyword_doc, parse_program, BLOCK_KEYWORDS, INFIX, SURFACE_KEYWORDS};
114
115    fn lowered(src: &str) -> String {
116        parse_program(src)
117            .unwrap_or_else(|e| panic!("`{src}` does not parse: {e}"))
118            .iter()
119            .map(ToString::to_string)
120            .collect::<Vec<_>>()
121            .join(" ")
122    }
123
124    /// Red run, 2026-09-29: changing the pipe row's `lowers_to` to
125    /// `(f a xs)` failed with "`xs |> f(a)` lowers to (f xs a), and its row
126    /// says (f a xs)".
127    #[test]
128    fn every_form_lowers_as_its_row_says() {
129        for form in FORMS {
130            let got = lowered(form.example);
131            assert_eq!(
132                got, form.lowers_to,
133                "`{}` lowers to {got}, and its row says {}",
134                form.example, form.lowers_to
135            );
136        }
137    }
138
139    /// Red run, 2026-09-29: emptying the `"%"` row's doc failed with
140    /// "the operator `%` has no doc"; deleting the `"case"` arm of
141    /// `keyword_doc` failed with "the keyword `case` has no doc".
142    #[test]
143    fn every_operator_and_keyword_is_described() {
144        for op in INFIX {
145            assert!(
146                !op.doc.trim().is_empty(),
147                "the operator `{}` has no doc",
148                op.op
149            );
150        }
151        for word in SURFACE_KEYWORDS.iter().chain(BLOCK_KEYWORDS) {
152            assert!(
153                !keyword_doc(word).trim().is_empty(),
154                "the keyword `{word}` has no doc"
155            );
156        }
157        for form in FORMS {
158            assert!(
159                !form.doc.trim().is_empty(),
160                "the form `{}` has no doc",
161                form.example
162            );
163        }
164    }
165}