""" Core types for the s-expression language. Symbol — unquoted identifier (e.g. div, ~card, map) Keyword — colon-prefixed key (e.g. :class, :id) Lambda — callable closure created by (lambda ...) or (fn ...) Nil — singleton null value """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any # --------------------------------------------------------------------------- # Nil # --------------------------------------------------------------------------- class _Nil: """Singleton nil value — falsy, serialises as 'nil'.""" _instance: _Nil | None = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __bool__(self): return False def __repr__(self): return "nil" def __eq__(self, other): return other is None or isinstance(other, _Nil) def __hash__(self): return hash(None) NIL = _Nil() # --------------------------------------------------------------------------- # Symbol # --------------------------------------------------------------------------- @dataclass(frozen=True) class Symbol: """An unquoted symbol/identifier.""" name: str def __repr__(self): return f"Symbol({self.name!r})" def __eq__(self, other): if isinstance(other, Symbol): return self.name == other.name if isinstance(other, str): return self.name == other return False def __hash__(self): return hash(self.name) @property def is_component(self) -> bool: """True if this symbol names a component (~prefix).""" return self.name.startswith("~") # --------------------------------------------------------------------------- # Keyword # --------------------------------------------------------------------------- @dataclass(frozen=True) class Keyword: """A keyword starting with colon (e.g. :class, :id).""" name: str def __repr__(self): return f"Keyword({self.name!r})" def __eq__(self, other): if isinstance(other, Keyword): return self.name == other.name return False def __hash__(self): return hash((":", self.name)) # --------------------------------------------------------------------------- # Lambda # --------------------------------------------------------------------------- @dataclass class Lambda: """A callable closure. Created by ``(lambda (x) body)`` or ``(fn (x) body)``. Captures the defining environment so free variables resolve correctly. """ params: list[str] body: Any closure: dict[str, Any] = field(default_factory=dict) name: str | None = None # optional, set by (define name (fn ...)) def __repr__(self): tag = self.name or "lambda" return f"<{tag}({', '.join(self.params)})>" def __call__(self, *args: Any, evaluator: Any = None, caller_env: dict | None = None) -> Any: """Invoke the lambda. Requires *evaluator* — the evaluate() function.""" if evaluator is None: raise RuntimeError("Lambda requires evaluator to be called") if len(args) != len(self.params): raise RuntimeError( f"{self!r} expects {len(self.params)} args, got {len(args)}" ) local = dict(self.closure) if caller_env: local.update(caller_env) for p, v in zip(self.params, args): local[p] = v return evaluator(self.body, local) # --------------------------------------------------------------------------- # Macro # --------------------------------------------------------------------------- @dataclass class Macro: """A macro — an AST-transforming function. Created by ``(defmacro name (params... &rest rest) body)``. Receives unevaluated arguments, evaluates its body to produce a new s-expression, which is then evaluated in the caller's environment. """ params: list[str] rest_param: str | None # &rest parameter name body: Any # unevaluated — returns an s-expression to eval closure: dict[str, Any] = field(default_factory=dict) name: str | None = None def __repr__(self): tag = self.name or "macro" return f"<{tag}({', '.join(self.params)})>" # --------------------------------------------------------------------------- # Component # --------------------------------------------------------------------------- @dataclass class Component: """A reusable UI component defined via ``(defcomp ~name (&key ...) body)``. Components are like lambdas but accept keyword arguments and support a ``children`` rest parameter. """ name: str params: list[str] # keyword parameter names (without &key prefix) has_children: bool # True if &rest children declared body: Any # unevaluated s-expression body closure: dict[str, Any] = field(default_factory=dict) css_classes: set[str] = field(default_factory=set) # pre-scanned :class values def __repr__(self): return f"" # --------------------------------------------------------------------------- # HandlerDef # --------------------------------------------------------------------------- @dataclass class HandlerDef: """A declarative fragment handler defined in an .sx file. Created by ``(defhandler name (&key param...) body)``. The body is evaluated in a sandboxed environment with only s-expression primitives available. """ name: str params: list[str] # keyword parameter names body: Any # unevaluated s-expression body closure: dict[str, Any] = field(default_factory=dict) def __repr__(self): return f"" # --------------------------------------------------------------------------- # RelationDef # --------------------------------------------------------------------------- @dataclass(frozen=True) class RelationDef: """A declared relation between two entity types. Created by ``(defrelation :name ...)`` s-expressions. """ name: str # "page->market" from_type: str # "page" to_type: str # "market" cardinality: str # "one-to-one" | "one-to-many" | "many-to-many" inverse: str | None # "market->page" nav: str # "submenu" | "tab" | "badge" | "inline" | "hidden" nav_icon: str | None # "fa fa-shopping-bag" nav_label: str | None # "markets" # --------------------------------------------------------------------------- # PageDef # --------------------------------------------------------------------------- @dataclass class PageDef: """A declarative GET page defined in an .sx file. Created by ``(defpage name :path "/..." :auth :public :content expr)``. Slots are stored as unevaluated AST and resolved at request time. """ name: str path: str auth: str | list # "public", "login", "admin", or ["rights", ...] layout: Any # layout name/config (unevaluated) cache: dict | None data_expr: Any # unevaluated AST content_expr: Any # unevaluated AST filter_expr: Any aside_expr: Any menu_expr: Any closure: dict[str, Any] = field(default_factory=dict) def __repr__(self): return f"" # --------------------------------------------------------------------------- # QueryDef / ActionDef # --------------------------------------------------------------------------- @dataclass class QueryDef: """A declarative data query defined in an .sx file. Created by ``(defquery name (&key param...) "docstring" body)``. The body is evaluated with async I/O primitives to produce JSON data. """ name: str params: list[str] # keyword parameter names doc: str # docstring body: Any # unevaluated s-expression body closure: dict[str, Any] = field(default_factory=dict) def __repr__(self): return f"" @dataclass class ActionDef: """A declarative action defined in an .sx file. Created by ``(defaction name (&key param...) "docstring" body)``. The body is evaluated with async I/O primitives to produce JSON data. """ name: str params: list[str] # keyword parameter names doc: str # docstring body: Any # unevaluated s-expression body closure: dict[str, Any] = field(default_factory=dict) def __repr__(self): return f"" # --------------------------------------------------------------------------- # StyleValue # --------------------------------------------------------------------------- @dataclass(frozen=True) class StyleValue: """A resolved CSS style produced by ``(css :flex :gap-4 :hover:bg-sky-200)``. Generated by the style resolver. The renderer emits ``class_name`` as a CSS class and registers the CSS rule for on-demand delivery. """ class_name: str # "sx-a3f2c1" declarations: str # "display:flex;gap:1rem" media_rules: tuple = () # ((query, decls), ...) pseudo_rules: tuple = () # ((selector, decls), ...) keyframes: tuple = () # (("spin", "@keyframes spin{...}"), ...) def __repr__(self): return f"" def __str__(self): return self.class_name # --------------------------------------------------------------------------- # Continuation # --------------------------------------------------------------------------- class Continuation: """A captured delimited continuation (shift/reset). Callable with one argument — provides the value that the shift expression "returns" within the delimited context. """ __slots__ = ("fn",) def __init__(self, fn): self.fn = fn def __call__(self, value=NIL): return self.fn(value) def __repr__(self): return "" class _ShiftSignal(BaseException): """Raised by shift to unwind to the nearest reset. Inherits from BaseException (not Exception) to avoid being caught by generic except clauses in user code. """ __slots__ = ("k_name", "body", "env") def __init__(self, k_name, body, env): self.k_name = k_name self.body = body self.env = env # --------------------------------------------------------------------------- # Type alias # --------------------------------------------------------------------------- # An s-expression value after evaluation SExp = int | float | str | bool | Symbol | Keyword | Lambda | Macro | Component | Continuation | HandlerDef | RelationDef | PageDef | QueryDef | ActionDef | StyleValue | list | dict | _Nil | None