1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Which head symbols INTRODUCE names — the one table every syntactic
//! walker over `Spanned` reads.
//!
//! WHY THIS LIVES IN THE BASE CRATE. Two passes in this workspace walk raw
//! (un-evaluated) source and must know where a name stops meaning what the
//! enclosing scope says it means:
//!
//! * `tatara_lisp_lint::rules::unbound_symbol` — a reference to a name
//! nothing binds. Its module docs record the measured cost of getting
//! this list wrong: a fleet sweep went 422 -> 87 false positives purely
//! by handling `defmacro`'s three-part shape, the lambda-list keywords,
//! the generic `def…` prefix and the `fn` / `λ` / `catch` aliases.
//! * `tatara_lisp_eval::build_check` — argument-count checking. A local
//! binder that shadows a top-level function name is the ONLY way that
//! pass can invent an arity error out of correct code
//! (`(define (twice f x) (f (f x)))` where `f` is also a 2-argument
//! top-level function), so it needs exactly the same table.
//!
//! Neither crate depends on the other — by design, so the linter cannot keep
//! a stale copy of the interpreter (see `unbound_symbol`'s module docs). That
//! makes `tatara-lisp`, which both already depend on, the only place a shared
//! table can live without inverting the layering. A second copy in the second
//! walker would be a table that is *free to disagree*, and the two walkers
//! disagreeing about what `catch` binds is precisely a false positive.
//!
//! This module is DATA ONLY — no walker, no scope stack. The two consumers
//! have genuinely different jobs (report an unknown name / count arguments)
//! and their walks are not the same function; what they share is the
//! vocabulary, and only the vocabulary is lifted here.
/// `(define NAME v)` / `(define (NAME params…) body…)`.
///
/// Kept separate from [`DEF_PREFIX`] because item 3 is a VALUE, not a
/// parameter list — the two shapes cannot share a walker arm.
pub const DEFINE_HEADS: & = &;
/// Lambda-shaped: a parameter list (or a bare rest symbol) followed by a
/// body. `fn` / `λ` are aliases; `catch` binds `(e)` exactly the same way.
pub const LAMBDA_HEADS: & = &;
/// `((name init) …)` bindings then a body. `let` evaluates initialisers in
/// the OUTER scope; `let*` / `letrec` see the names bound so far.
pub const LET_HEADS: & = &;
/// Contents are data, never references.
pub const QUOTE_HEADS: & = &;
/// Prefix for every other definition form — `defmacro`, plus driver and
/// user-macro heads (`deftest`, `defphase`, `defreversal`, …). A prefix
/// rather than a list because the set is OPEN: those heads may be defined in
/// a file the walker is not looking at, or by a test driver rather than by
/// the interpreter.
pub const DEF_PREFIX: &str = "def";
/// Markers inside a parameter list, never value references — and never a
/// positional parameter either, which is why an arity checker must treat a
/// signature containing one as VARIADIC rather than counting the symbols.
pub const LAMBDA_LIST_KEYWORDS: & =
&;
/// Does this parameter-list entry mark the end of the fixed positional
/// parameters? Used by an arity checker to decline to claim an arity.
/// Does this head introduce names into a scope its body can see?
///
/// `define` and the `def…` family bind into the ENCLOSING scope as well, so
/// a caller that only needs "should I push a scope here?" gets `true` for
/// all of them; deciding *which* names go where stays with the caller, whose
/// two consumers genuinely differ.