Tramaj.Ast
- Package
- purescript-tramaj-purs
- Repository
- lucasdicioccio/purescript-tramaj-purs
The core semantic AST: what a Tramaj program means, after the parser has desugared everything the surface syntax offers on top of it.
This is a semantic representation, not a parser representation: no
source locations, no comments, no formatting, no recovery nodes.
Surface conveniences — string interpolation, object shorthand,
@name=expr binding lines, the multi-armed branch(...) form — have
no constructors here; Tramaj.Parser lowers them into the
constructors below.
The defining change from v1: documents are expressions. Element
and Fragment are ordinary Expr constructors, so a document node is
a value that can be bound, passed to a lambda, returned from one, or
stored in an array like any other. v1's separate template-phase AST —
and with it the duplicated template-phase map/branch/value forms —
is gone.
Where an invalid program can be made unrepresentable, it is: an import
name, an action's event and key, and an action adaptation's prefix are
all String rather than Expr, because the language's
static-analysis guarantees (see Tramaj.Analysis) depend on them
being knowable without evaluating anything.
See specs/reference.md for the language and specs/decisions.md for
which reading of the (now archived) design drafts won where they
disagreed. Kept in lockstep with ../tramaj-hs/src/Tramaj/Ast.hs.
#Program Source
data ProgramA whole program, distinguished by what its root produces rather than by what it may contain — both halves share one expression language.
A DocumentProgram evaluates to a Tramaj.Node document; an
ExpressionProgram evaluates to an ordinary JSON value, for hosts
that want the data half of the language on its own. The parser tells
them apart syntactically: a root beginning with . is a document.
Constructors
Instances
#Expr Source
data ExprPath root fields — a name in the environment plus static field
segments: $ctx.user.name is Path "ctx" [ "user", "name" ]. Dynamic
access goes through the lookup builtin instead.
FieldAccess is field access on an arbitrary expression, which Path
cannot express because its root must be a name:
import("x", {}).vals.button.
Call's function is an Expr, not a name, because builtins are
ordinary values in the initial environment — so cardinality($xs),
$f($x) and map($xs, $not) all travel the same path.
Lambda closes over the environment where it is evaluated. Multiple
parameters, since fold/scan need (acc, item) and the language
has no currying. No recursion: a binding's value is not in scope
while that value is being evaluated.
Let is one lexical binding. The surface @name=expr block lowers to
nested Lets, which is why a binding may reference earlier ones but
not later ones.
Element tag attributes value children — the value slot is NullLit
unless the template sets one; see Tramaj.Node for what it is for.
Fragment is sibling nodes with no wrapper element — the
JSX-children case, expressible as an ordinary value because documents
are expressions.
Branch evaluates its condition, then only the arm it selects. This
is the one place the language departs from evaluating arguments
eagerly, and it is why Branch is a constructor rather than a
builtin: errors in an unreached arm must not surface. The surface's
multi-armed branch(fallback, p1, v1, ...) lowers to nested Branch.
Concat is the monoid operation a <> b over String, Array or
Object — same type on both sides, right-biased on object key
collisions.
Import name parameters is a statically named dependency and the
parameters supplied to it here — not necessarily all of them. An
import that omits a parameter its library reads is saturated later, by
calling the import value with more parameters; there is deliberately
no partial-import constructor, because partiality is not a property of
the syntax at all, only of which names have arrived by the time the
library runs.
AdaptActions target adaptation fn prefixes every action key in a
document subtree, with an optional closure for the event type and
payload. The prefix is static so the action vocabulary stays
enumerable without evaluation.
Constructors
Path String (Array String)FieldAccess Expr (Array String)Call Expr (Array Expr)Lambda (Array String) ExprLet String Expr ExprStringLit StringNumberLit NumberBoolLit BooleanNullLitArrayLit (Array Expr)ObjectLit (Array (Tuple String Expr))Element String (Array Attribute) Expr (Array Expr)Fragment (Array Expr)Branch Expr Expr ExprMap Expr ExprFilter Expr ExprScan Expr Expr ExprFold Expr Expr ExprConcat Expr ExprImport String (Array (Tuple String ParamValue))AdaptActions Expr ActionAdaptation (Maybe Expr)Constrain String (Array Expr)Emit Expr ExprAlloc Int ExprDemand (Array String)TypeDecl String TypeExpr ExprTypeAnnotate String TypeExpr Expr ExprTypeEmit String (Array TypeConstraintArg) Expr
Instances
#Attribute Source
data AttributeBoth attribute-position constructs. An action's event and key are
String, not Expr: the payload stays fully dynamic, but the
identifying half is static, which is what makes the set of actions a
program can emit computable ahead of time.
Constructors
Instances
#ParamValue Source
data ParamValueHow one import parameter gets its value. There are three ways to
supply one and only two of them are here — the third is leaving it
out (see Import).
PExpr is any expression, evaluated where the import is written.
PFromContext path is ctx(spec.replicas): the value comes from the
importing program's own context, read where the import is written.
It means what $ctx.spec.replicas means and evaluates identically —
the point of the separate node is entirely static. A path sitting in
the AST is a hole Tramaj.Analysis can enumerate directly, whereas
the same read spelled $ctx.spec.replicas is an ordinary Path
buried in an arbitrary expression, indistinguishable from every other
read. Writing ctx(...) is how an author says "this is a hole, count
it".
%T or %ctx.path in a params record (v4-types §2): a type argument,
told apart from PExpr/PFromContext purely by the leading % the
parser already saw — the params record stays one syntactic form even
though it carries two semantic channels (§2.1). PType carries a full
TypeExpr rather than just a name, since %[T], %{a:T} and
forwarding %ctx.path are all legal here, not only a bare reference.
Constructors
Instances
#ActionAdaptation Source
data ActionAdaptationRestricted on purpose to two forms rather than an arbitrary
key-rewriting function. Adaptations compose the obvious way: prefixing
a: and then b: gives b:a:key, needing no "already adapted"
state.
Constructors
Instances
#Stmt Source
data StmtOne statement of the surface program grammar (v3-symbols §2.2), after
lowering: a binding or an emission. Both nest into the same
Let/Emit chain; this is only unlets' view of it.
Constructors
SLet String ExprSEmit ExprSTypeDecl String TypeExprSAnnotate String TypeExpr ExprSTypeEmit String (Array TypeConstraintArg)
Instances
#TypeExpr Source
data TypeExprThe type algebra (v4-types §1), before resolution. Six shapes there
become six here, but Ref is split into two pre-resolution forms:
TName for a bare local name (Point) and TLibRef for a name reached
through an import ($msg.types.Envelope). Resolution (v4-types §9, not
yet implemented) is what turns either into a genuine (library, name)
reference or reports UnresolvedType — parsing alone cannot tell a
declaration name from a typo.
TPrim is recognized here, at parse time, rather than left for
resolution to classify: the five primitive names are a closed, reserved
lexical set (v4-types §1), not ordinary identifiers that happen to
resolve to a primitive.
Constructors
TPrim StringTArray TypeExprTRecord (Array (Tuple String TypeExpr))TUnion (Array (Tuple String (Maybe TypeExpr)))TName StringTLibRef String StringTVar (Array String)
Instances
#TypeConstraintArg Source
data TypeConstraintArgOne argument to !type-constraint (v4-types §5): a "%"-marked type
expression, or a literal scalar. A bare Expr would overreach — §5
fixes the argument grammar to exactly these two shapes, computed
nowhere, which is what keeps a type constraint statically collectible
the way an ordinary constraint's name is (v3-symbols §2.1) but its
arguments are not.
Constructors
Instances
#programRoot Source
programRoot :: Program -> Expr#unlets Source
unlets :: Expr -> { root :: Expr, statements :: Array Stmt }Peels the outermost Let/Emit chain back off, inverting stmts.
Nothing but a program's surface statement block can put a Let or
Emit outermost, so this recovers exactly that block, in order.
Keeping statements as ordinary nested constructors in the AST, and
recovering the block when it is needed, avoids a second binding
construct whose scoping would have to be specified separately.
Stopping at the first Let alone — as this once did, before Emit
existed — silently truncates a binding block that has a ! statement
threaded through it: @a=1; !c; @b=2; root would report only a.
runLibrary is the caller this matters to, since it is what exposes
.vals and is also the one place a library's own emissions must be
evaluated.
#letBindings Source
letBindings :: Array Stmt -> Array (Tuple String Expr)Just the named bindings of a statement block, in order — what an
import exposes as .vals. An annotated binding (@x : T = e) still
binds x, so it counts here exactly as a plain SLet does; only
emissions and type declarations are discarded positionally. A caller
that must also evaluate every statement (running a library) should fold
over unlets' full result instead.
#isHiddenName Source
isHiddenName :: String -> BooleanA name the parser invents when lowering a binding pattern
(decisions §17): it starts with #, which no surface name can. Such a
binding is a lowering detail, so it is never reported as a symbol's
"binding" and never exposed through a library's .vals.
#typeDecls Source
typeDecls :: Array Stmt -> Array (Tuple String TypeExpr)Just the type declarations of a statement block, in order — the
analogue of letBindings for the type realm (v4-types §1.1). This is
what a later resolution pass (v4-types §9) collects before resolving
any of them, so that a self- or mutually-recursive declaration is
visible to the pass before it looks anything up.
#adaptKey Source
adaptKey :: ActionAdaptation -> String -> StringHow an adaptation rewrites one action key. Shared by evaluation and by static analysis, which is the point of restricting adaptation to these two forms: the analysis can apply the very same function the evaluator will, without evaluating anything.
#subExprs Source
subExprs :: Expr -> Array ExprThe immediately-contained expressions of an expression, in source order.
Written once here so that the analyses in Tramaj.Analysis are a few
lines each rather than a 20-case fold apiece — and, more importantly,
so that adding a constructor to Expr has exactly one place to be
taught about instead of one per traversal.
#attributeExprs Source
attributeExprs :: Array Attribute -> Array ExprThe computed expressions in a list of attributes — an ordinary attribute's value or an action's payload. An action's event and key are static text, so they are not expressions to walk into.
#numberAllocs Source
numberAllocs :: Expr -> ExprAssigns each Alloc in a program the index of its ?(...) among all
of them, in source order (v3-symbols §1.4) — a plain pre-order,
left-to-right walk over the freshly parsed tree, numbering as it goes.
Done as a rewrite after parsing, rather than by a counter threaded
through the parser itself, so that two independently written parsers
agree on site numbers by construction: both run this same function
over what they parsed, rather than having to agree on a
parser-internal traversal order. It relies on one property of every
constructor below: each rebuilds a node from the same immediate
subexpressions subExprs would report, in the same order, which is
what makes a left-to-right pre-order walk here land on exactly "the
n-th ? in the source" for every shape the grammar produces —
attributes before value before children in an Element, condition
before either arm in a Branch, and so on.