Skip to main content

pointlock_ir/
expr.rs

1//! The non-Turing-complete expression AST (spine §7, 02 §8).
2//!
3//! IR expressions are data (a JSON AST), never strings: the YAML surface
4//! `${{ ... }}` is compiled away in `parse`/`normalize`, and the runner has
5//! no parser and no eval. Purity (no loops, no user functions, no I/O, no
6//! clock) is what makes offline re-judging (`judgeDirty` alignment)
7//! mathematically sound.
8
9use std::borrow::Cow;
10use std::collections::BTreeMap;
11
12use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
13use serde::{Deserialize, Serialize};
14
15use crate::primitives::{Identifier, RefPath};
16
17/// Expression node: exactly one of `lit` / `ref` / `fn` (02 §8.1).
18///
19/// Wire shape is the baseline schema's `oneOf` of three closed single-key
20/// objects; the variants are mutually exclusive by their required keys, so
21/// serde's untagged representation is deterministic.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
23#[serde(untagged)]
24pub enum Expr {
25    /// Literal JSON value.
26    Lit(LitExpr),
27    /// Reference into the closed scope grammar.
28    Ref(RefExpr),
29    /// Whitelisted pure-function application.
30    Fn(FnExpr),
31}
32
33impl Expr {
34    /// Builds a literal expression.
35    pub fn lit(value: impl Into<serde_json::Value>) -> Self {
36        Expr::Lit(LitExpr { lit: value.into() })
37    }
38
39    /// Builds a reference expression.
40    pub fn reference(path: RefPath) -> Self {
41        Expr::Ref(RefExpr { r#ref: path })
42    }
43
44    /// Builds a pure-function application. Arity/type constraints are
45    /// enforced by the schema and by the compiler `check` phase, not here.
46    pub fn call(f: PureFn, args: Vec<Expr>) -> Self {
47        Expr::Fn(FnExpr { r#fn: f, args })
48    }
49}
50
51/// Literal JSON value (any JSON type, including null).
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
53#[serde(deny_unknown_fields)]
54pub struct LitExpr {
55    /// The literal value, verbatim.
56    pub lit: serde_json::Value,
57}
58
59/// Reference expression: a [`RefPath`] into the closed scope grammar.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
61#[serde(deny_unknown_fields)]
62pub struct RefExpr {
63    /// The dotted reference path.
64    pub r#ref: RefPath,
65}
66
67/// Whitelisted pure-function application.
68///
69/// Arity is enforced by the schema (the `allOf` conditionals below mirror the
70/// baseline); argument types plus the literal-only constraints (`jsonPath`
71/// path, `regexMatch` pattern/flags) are enforced in the compiler `check`
72/// phase (02 §8.2).
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
74#[serde(deny_unknown_fields)]
75#[schemars(extend("allOf" = [
76    { "if": { "properties": { "fn": { "enum": ["eq", "ne", "jsonPath"] } } },
77      "then": { "properties": { "args": { "minItems": 2, "maxItems": 2 } } } },
78    { "if": { "properties": { "fn": { "enum": ["not", "len"] } } },
79      "then": { "properties": { "args": { "minItems": 1, "maxItems": 1 } } } },
80    { "if": { "properties": { "fn": { "enum": ["and", "or", "coalesce"] } } },
81      "then": { "properties": { "args": { "minItems": 2 } } } },
82    { "if": { "properties": { "fn": { "const": "concat" } } },
83      "then": { "properties": { "args": { "minItems": 1 } } } },
84    { "if": { "properties": { "fn": { "const": "regexMatch" } } },
85      "then": { "properties": { "args": { "minItems": 2, "maxItems": 3 } } } }
86]))]
87pub struct FnExpr {
88    /// The pure function to apply.
89    pub r#fn: PureFn,
90    /// Ordered argument expressions.
91    pub args: Vec<Expr>,
92}
93
94/// The closed pure-function whitelist (spine §7 / A.4, 02 §8.2).
95#[derive(
96    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
97)]
98#[serde(rename_all = "camelCase")]
99pub enum PureFn {
100    /// `(T, T) → boolean`
101    Eq,
102    /// `(T, T) → boolean`
103    Ne,
104    /// `(boolean) → boolean`
105    Not,
106    /// `(boolean…) → boolean` (arity ≥ 2)
107    And,
108    /// `(boolean…) → boolean` (arity ≥ 2)
109    Or,
110    /// `(string…) → string` (arity ≥ 1)
111    Concat,
112    /// `(string | array) → number`
113    Len,
114    /// `(T?, …, T) → T` — first non-absent value (arity ≥ 2)
115    Coalesce,
116    /// `(any, string) → any` — path must be a literal string
117    JsonPath,
118    /// `(string, string[, string]) → boolean` — pattern/flags must be literals
119    RegexMatch,
120}
121
122/// Identifier-keyed map of expressions (baseline exemption class 2: keys are
123/// data, constrained by `propertyNames`; values are strongly typed).
124///
125/// Backed by a `BTreeMap` so serialization order is deterministic — a
126/// prerequisite of the canonical form (02 §12.1), where map member order is
127/// JCS-sorted and carries no semantics.
128#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
129#[serde(transparent)]
130pub struct ExprMap(pub BTreeMap<Identifier, Expr>);
131
132impl ExprMap {
133    /// Creates an empty map.
134    pub fn new() -> Self {
135        Self::default()
136    }
137}
138
139impl std::ops::Deref for ExprMap {
140    type Target = BTreeMap<Identifier, Expr>;
141    fn deref(&self) -> &Self::Target {
142        &self.0
143    }
144}
145
146impl std::ops::DerefMut for ExprMap {
147    fn deref_mut(&mut self) -> &mut Self::Target {
148        &mut self.0
149    }
150}
151
152impl From<BTreeMap<Identifier, Expr>> for ExprMap {
153    fn from(map: BTreeMap<Identifier, Expr>) -> Self {
154        Self(map)
155    }
156}
157
158impl FromIterator<(Identifier, Expr)> for ExprMap {
159    fn from_iter<I: IntoIterator<Item = (Identifier, Expr)>>(iter: I) -> Self {
160        Self(iter.into_iter().collect())
161    }
162}
163
164impl JsonSchema for ExprMap {
165    fn schema_name() -> Cow<'static, str> {
166        Cow::Borrowed("ExprMap")
167    }
168    fn schema_id() -> Cow<'static, str> {
169        Cow::Borrowed("pointlock_ir::ExprMap")
170    }
171    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
172        json_schema!({
173            "type": "object",
174            "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
175            "additionalProperties": generator.subschema_for::<Expr>(),
176            "description": "Identifier-keyed map of expressions (exemption class 2: keys are data, constrained by propertyNames)."
177        })
178    }
179}