axon-frontend 1.8.0

AXON compiler frontend — lexer, parser, AST, epistemic type system, type checker, IR generator. Zero runtime dependencies. v1.3.0 lowers requires_capabilities into IRAxonEndpoint (the PCC capability-containment property, §Fase 51.x.1). v1.2.0 added PRIMITIVE_REGISTRY — closed catalogue of every named language construct (45 entries) with PrimitiveInfo / DocStatus / CoverageSummary types + helpers. v1.1.0 introduced session-types + multiparty projection. See https://github.com/Bemarking/axon-lang.
Documentation
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
//! §Fase 38.b (D1) — the closed `axonstore` column-schema catalog,
//! Rust frontend side.
//!
//! Three closed forms an `axonstore` may declare its column schema in:
//!
//!  - **Inline** — `schema { col: Type [constraint…], … }`. The column
//!    schema lives in source. Use case: small static schemas, the
//!    schema that ships with the application source.
//!  - **Manifest reference** — `schema: "qualified.name"`. The column
//!    schema lives in a checked-in `.axon-schema.yml` (or
//!    `.axon-schema.json`) manifest, referenced by qualified name. Use
//!    case: large schemas, schemas captured by `axon store introspect`
//!    against an existing database.
//!  - **Per-tenant env-var schema namespace** — `schema: env:VAR` (or
//!    quoted `schema: "env:VAR"`). The schema NAMESPACE (e.g.
//!    `tenant_42`) is resolved at deploy time from the named
//!    environment variable; the columns themselves come from a
//!    manifest entry keyed on the resolved namespace + table name.
//!    Use case: schema-per-tenant topology.
//!
//! This module defines the AST surface only — the type-checker proof
//! against these declarations lives in §38.d / §38.e (the
//! `StoreColumnProof` pass), shipping in subsequent sub-fases.
//!
//! Mirror: `axon/compiler/ast_nodes.py` (`StoreSchemaNode`,
//! `StoreColumnNode`) — the Python frontend has carried an
//! inline-form-only surface as forward-compat dead code since v1.30.0;
//! Fase 38.b makes both sides authoritative, brings the Rust side to
//! parity, and adds the new manifest-ref + env-var forms cross-stack.

use crate::tokens::Trivia;

// ════════════════════════════════════════════════════════════════════
//  D1 — the closed 15-type catalog (compile-time mirror of the v1.30.0
//  `PgTypeClass` runtime catalog)
// ════════════════════════════════════════════════════════════════════

/// The closed column-type catalog an `axonstore` may declare a column
/// as. Mirrors the v1.30.0 [`crate::ir_nodes::IRStoreColumnType`]
/// surface and the Postgres runtime's `PgTypeClass` (in
/// `axon-rs/src/store/postgres_backend.rs`) one-for-one.
///
/// Source-level surface accepts both the canonical PascalCase name
/// AND a small set of common lowercase aliases (`int` for `Int`,
/// `boolean` for `Bool`, `integer` for `Int`, …) — see
/// [`StoreColumnType::from_token`]. The AST always carries the
/// canonical PascalCase variant; the alias is normalized at parse
/// time.
///
/// A column whose declared type is OUTSIDE this catalog is a parse
/// error at `axon check` time with a precise message + Levenshtein
/// suggestions. The honest-scope boundary is named: Postgres types
/// outside the catalog — `enum`, `domain`, array, `citext`, PostGIS
/// `geometry`, custom composites — remain `UnsupportedColumnType`,
/// tracked for the Fase 38+ "broaden the catalog" follow-on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StoreColumnType {
    Uuid,
    Text,
    Int,
    BigInt,
    Float,
    Double,
    Bool,
    Timestamptz,
    Timestamp,
    Date,
    Time,
    Jsonb,
    Json,
    Bytea,
    Numeric,
}

impl StoreColumnType {
    /// The closed catalog, in canonical declaration order — useful for
    /// exhaustive iteration in tests + the smart-suggest dictionary.
    pub const ALL: &'static [StoreColumnType] = &[
        StoreColumnType::Uuid,
        StoreColumnType::Text,
        StoreColumnType::Int,
        StoreColumnType::BigInt,
        StoreColumnType::Float,
        StoreColumnType::Double,
        StoreColumnType::Bool,
        StoreColumnType::Timestamptz,
        StoreColumnType::Timestamp,
        StoreColumnType::Date,
        StoreColumnType::Time,
        StoreColumnType::Jsonb,
        StoreColumnType::Json,
        StoreColumnType::Bytea,
        StoreColumnType::Numeric,
    ];

    /// The canonical PascalCase declaration name — exactly what an
    /// adopter writes in source and exactly what the IR / manifest
    /// serializes as. Stable surface — adopters tooling can rely on it.
    pub fn canonical_name(self) -> &'static str {
        match self {
            StoreColumnType::Uuid => "Uuid",
            StoreColumnType::Text => "Text",
            StoreColumnType::Int => "Int",
            StoreColumnType::BigInt => "BigInt",
            StoreColumnType::Float => "Float",
            StoreColumnType::Double => "Double",
            StoreColumnType::Bool => "Bool",
            StoreColumnType::Timestamptz => "Timestamptz",
            StoreColumnType::Timestamp => "Timestamp",
            StoreColumnType::Date => "Date",
            StoreColumnType::Time => "Time",
            StoreColumnType::Jsonb => "Jsonb",
            StoreColumnType::Json => "Json",
            StoreColumnType::Bytea => "Bytea",
            StoreColumnType::Numeric => "Numeric",
        }
    }

    /// Parse a source-level token (an identifier or keyword) into a
    /// catalog variant. Accepts the canonical name AND a small set of
    /// common aliases — case-insensitive at the level of the alias
    /// table to maximise ergonomics, but the AST always carries the
    /// canonical variant so the IR is deterministic.
    ///
    /// Aliases (D5 ergonomic floor — not load-bearing, not promised in
    /// the public contract; the canonical name is the supported form):
    ///
    ///   - `int`, `integer`, `int4` → `Int`
    ///   - `bigint`, `int8` → `BigInt`
    ///   - `bool`, `boolean` → `Bool`
    ///   - `text`, `varchar`, `string` → `Text`
    ///   - `uuid` → `Uuid`
    ///   - `float`, `float4`, `real` → `Float`
    ///   - `double`, `float8` → `Double`
    ///   - `timestamptz` → `Timestamptz`
    ///   - `timestamp` → `Timestamp`
    ///   - `date` → `Date`
    ///   - `time` → `Time`
    ///   - `jsonb` → `Jsonb`
    ///   - `json` → `Json`
    ///   - `bytea` → `Bytea`
    ///   - `numeric`, `decimal` → `Numeric`
    ///
    /// Anything else returns `None` — the parser surfaces it as an
    /// `axon-T8xx`-class error with the closed-catalog list.
    pub fn from_token(name: &str) -> Option<StoreColumnType> {
        // Canonical (PascalCase) lookup first — exact-match.
        for &t in Self::ALL {
            if t.canonical_name() == name {
                return Some(t);
            }
        }
        // Alias table — case-insensitive on the source token.
        match name.to_ascii_lowercase().as_str() {
            "int" | "integer" | "int4" => Some(StoreColumnType::Int),
            "bigint" | "int8" => Some(StoreColumnType::BigInt),
            "bool" | "boolean" => Some(StoreColumnType::Bool),
            "text" | "varchar" | "string" => Some(StoreColumnType::Text),
            "uuid" => Some(StoreColumnType::Uuid),
            "float" | "float4" | "real" => Some(StoreColumnType::Float),
            "double" | "float8" => Some(StoreColumnType::Double),
            "timestamptz" => Some(StoreColumnType::Timestamptz),
            "timestamp" => Some(StoreColumnType::Timestamp),
            "date" => Some(StoreColumnType::Date),
            "time" => Some(StoreColumnType::Time),
            "jsonb" => Some(StoreColumnType::Jsonb),
            "json" => Some(StoreColumnType::Json),
            "bytea" => Some(StoreColumnType::Bytea),
            "numeric" | "decimal" => Some(StoreColumnType::Numeric),
            _ => None,
        }
    }

    /// All canonical names — useful for the smart-suggest dictionary
    /// when the parser rejects an unknown type.
    pub fn all_canonical_names() -> Vec<&'static str> {
        Self::ALL.iter().map(|t| t.canonical_name()).collect()
    }
}

impl std::fmt::Display for StoreColumnType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.canonical_name())
    }
}

// ════════════════════════════════════════════════════════════════════
//  AST nodes — inline schema form
// ════════════════════════════════════════════════════════════════════

/// One column entry in an inline `schema { col: Type [constraint…], … }`
/// block.
///
/// The closed-set constraint vocabulary is shared with the Python AST
/// (`StoreColumnNode`): `primary_key`, `auto_increment`, `not_null`,
/// `unique`, `default <literal>`.
#[derive(Debug, Clone)]
pub struct StoreColumn {
    pub name: String,
    pub col_type: StoreColumnType,
    pub primary_key: bool,
    pub auto_increment: bool,
    pub not_null: bool,
    pub unique: bool,
    /// Literal default value, source-text verbatim. The runtime does
    /// not interpolate; the database supplies the default. Empty when
    /// no `default …` constraint is declared.
    pub default_value: String,
    /// §Fase 38.x.c (D2) — `true` iff this column is declared with
    /// `GENERATED ALWAYS AS IDENTITY` or `GENERATED BY DEFAULT AS
    /// IDENTITY` in the live database (`pg_attribute.attidentity` is
    /// `'a'` or `'d'`). Distinct from `auto_increment` (which marks
    /// the legacy SERIAL pattern via a `nextval(...)` default
    /// expression). T803 treats an `identity` column as safe-to-omit
    /// from a `persist` because Postgres auto-fills it.
    ///
    /// Backwards-compatibility (D5): the field defaults to `false`,
    /// matching v1.38.2 behavior for every column. A manifest written
    /// against v1.38.2 round-trips byte-identically.
    pub identity: bool,
    pub line: u32,
    pub column: u32,
}

// ════════════════════════════════════════════════════════════════════
//  AST node — the three closed `schema:` declaration forms
// ════════════════════════════════════════════════════════════════════

/// §Fase 38.b (D1) — the three closed forms an `axonstore` may declare
/// its column schema in. The AST captures the form; the §38.d / §38.e
/// `StoreColumnProof` pass consumes the resolved column set (regardless
/// of form) and proves every store reference against it.
///
/// `pub` so consumers (the type-checker, the runtime registry, the LSP)
/// can match exhaustively. Variants are `#[non_exhaustive]`-style only
/// at the doc level — additions go through a plan ratification per the
/// founder discipline.
#[derive(Debug, Clone)]
pub enum StoreColumnSchema {
    /// Form (a) — `schema { col: Type [constraint…], … }`.
    Inline {
        columns: Vec<StoreColumn>,
        /// Trivia attached to the opening `schema` keyword.
        leading_trivia: Vec<Trivia>,
        line: u32,
        column: u32,
    },
    /// Form (b) — `schema: "qualified.name"`. The qualified name
    /// resolves against a checked-in manifest entry (`.axon-schema.yml`
    /// / `.axon-schema.json`) at `axon check` time.
    ManifestRef {
        qualified_name: String,
        line: u32,
        column: u32,
    },
    /// Form (c) — `schema: env:VAR` (or quoted `schema: "env:VAR"`).
    /// The env-var resolves to the schema NAMESPACE at deploy time;
    /// the manifest then provides the column set for `<namespace>.<table>`.
    EnvVar {
        /// The env-var name (no `env:` prefix; the prefix was stripped
        /// at parse time).
        var_name: String,
        line: u32,
        column: u32,
    },
}

impl StoreColumnSchema {
    /// `true` iff this is the inline form. Convenience for the
    /// §38.d / §38.e type-checker, which can short-circuit a manifest
    /// lookup when the columns are already in the AST.
    pub fn is_inline(&self) -> bool {
        matches!(self, StoreColumnSchema::Inline { .. })
    }

    /// Returns the inline columns when the form is inline; `None`
    /// otherwise. The type-checker uses this to obtain the column
    /// set without a manifest round-trip.
    pub fn inline_columns(&self) -> Option<&[StoreColumn]> {
        match self {
            StoreColumnSchema::Inline { columns, .. } => Some(columns),
            _ => None,
        }
    }

    /// The source location of the `schema` keyword, for diagnostic
    /// rendering (the Fase 28 source-context block points at this).
    pub fn loc(&self) -> (u32, u32) {
        match self {
            StoreColumnSchema::Inline { line, column, .. }
            | StoreColumnSchema::ManifestRef { line, column, .. }
            | StoreColumnSchema::EnvVar { line, column, .. } => (*line, *column),
        }
    }

    /// A short form name (`"inline"` / `"manifest_ref"` / `"env_var"`)
    /// for diagnostic prose + the IR's tagged-union serialization.
    pub fn form_name(&self) -> &'static str {
        match self {
            StoreColumnSchema::Inline { .. } => "inline",
            StoreColumnSchema::ManifestRef { .. } => "manifest_ref",
            StoreColumnSchema::EnvVar { .. } => "env_var",
        }
    }
}

// ════════════════════════════════════════════════════════════════════
//  Unit tests — the closed catalog + parse/canonical-form discipline
// ════════════════════════════════════════════════════════════════════

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn catalog_has_exactly_15_variants() {
        // The plan-vivo §4 D1 commits to exactly 15 types. A future
        // catalog broadening goes through a plan ratification — this
        // pin catches an accidental addition.
        assert_eq!(StoreColumnType::ALL.len(), 15);
    }

    #[test]
    fn every_variant_has_a_unique_canonical_name() {
        let mut names: Vec<&'static str> =
            StoreColumnType::ALL.iter().map(|t| t.canonical_name()).collect();
        names.sort();
        let total = names.len();
        names.dedup();
        assert_eq!(
            names.len(),
            total,
            "canonical names must be unique across the catalog"
        );
    }

    #[test]
    fn every_canonical_name_parses_back_to_its_variant() {
        for &t in StoreColumnType::ALL {
            assert_eq!(
                StoreColumnType::from_token(t.canonical_name()),
                Some(t),
                "{} did not round-trip",
                t.canonical_name()
            );
        }
    }

    #[test]
    fn common_aliases_resolve_to_the_canonical_variant() {
        for (alias, expected) in [
            ("int", StoreColumnType::Int),
            ("integer", StoreColumnType::Int),
            ("int4", StoreColumnType::Int),
            ("bigint", StoreColumnType::BigInt),
            ("int8", StoreColumnType::BigInt),
            ("bool", StoreColumnType::Bool),
            ("boolean", StoreColumnType::Bool),
            ("text", StoreColumnType::Text),
            ("varchar", StoreColumnType::Text),
            ("string", StoreColumnType::Text),
            ("uuid", StoreColumnType::Uuid),
            ("float", StoreColumnType::Float),
            ("real", StoreColumnType::Float),
            ("double", StoreColumnType::Double),
            ("float8", StoreColumnType::Double),
            ("numeric", StoreColumnType::Numeric),
            ("decimal", StoreColumnType::Numeric),
            ("timestamptz", StoreColumnType::Timestamptz),
            ("timestamp", StoreColumnType::Timestamp),
            ("date", StoreColumnType::Date),
            ("time", StoreColumnType::Time),
            ("jsonb", StoreColumnType::Jsonb),
            ("json", StoreColumnType::Json),
            ("bytea", StoreColumnType::Bytea),
        ] {
            assert_eq!(
                StoreColumnType::from_token(alias),
                Some(expected),
                "alias `{alias}` did not resolve to `{}`",
                expected.canonical_name()
            );
        }
    }

    #[test]
    fn alias_lookup_is_case_insensitive_on_the_alias_table() {
        // Adopter ergonomics — the alias table tolerates case.
        // (Canonical names match exact-case; aliases are case-insensitive.)
        assert_eq!(StoreColumnType::from_token("INTEGER"), Some(StoreColumnType::Int));
        assert_eq!(StoreColumnType::from_token("Boolean"), Some(StoreColumnType::Bool));
        assert_eq!(StoreColumnType::from_token("UUID"), Some(StoreColumnType::Uuid));
    }

    #[test]
    fn unknown_type_names_return_none() {
        for unknown in [
            "Money", "Interval", "Cidr", "Inet", "Macaddr", "Geometry",
            "enum", "domain", "citext", "array", "anything", "", "   ",
            "Tier", "MyCustomType",
        ] {
            assert_eq!(
                StoreColumnType::from_token(unknown),
                None,
                "unknown type `{unknown}` must not resolve"
            );
        }
    }

    #[test]
    fn display_is_canonical_name() {
        for &t in StoreColumnType::ALL {
            assert_eq!(t.to_string(), t.canonical_name());
        }
    }

    #[test]
    fn schema_form_names_are_the_three_closed_forms() {
        let inline = StoreColumnSchema::Inline {
            columns: vec![],
            leading_trivia: vec![],
            line: 0,
            column: 0,
        };
        let manifest_ref = StoreColumnSchema::ManifestRef {
            qualified_name: "public.tenants".into(),
            line: 0,
            column: 0,
        };
        let env_var = StoreColumnSchema::EnvVar {
            var_name: "TENANT_SCHEMA".into(),
            line: 0,
            column: 0,
        };
        assert_eq!(inline.form_name(), "inline");
        assert_eq!(manifest_ref.form_name(), "manifest_ref");
        assert_eq!(env_var.form_name(), "env_var");
        assert!(inline.is_inline());
        assert!(!manifest_ref.is_inline());
        assert!(!env_var.is_inline());
        assert!(inline.inline_columns().is_some());
        assert!(manifest_ref.inline_columns().is_none());
        assert!(env_var.inline_columns().is_none());
    }
}