Module

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 Program

A 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 Expr

Path 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

Instances

#Attribute Source

data Attribute

Both 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 ParamValue

How 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 ActionAdaptation

Restricted 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 Stmt

One 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

Instances

#TypeExpr Source

data TypeExpr

The 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

Instances

#TypeConstraintArg Source

data TypeConstraintArg

One 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

#stmts Source

stmts :: Array Stmt -> Expr -> Expr

Nests a sequence of statements around a body, innermost last — the lowering the surface statement* expr program grammar uses (v3-symbols §2.2): a binding becomes a Let, an emission an Emit, a type declaration a TypeDecl, each wrapping everything that follows it.

#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 -> Boolean

A 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 -> String

How 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 Expr

The 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 Expr

The 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 -> Expr

Assigns 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.