Tramaj.Analysis
- Package
- purescript-tramaj-purs
- Repository
- lucasdicioccio/purescript-tramaj-purs
Static analysis: what a program depends on, what it can emit, and what it still needs — all answered by walking the AST, without evaluating it, without a context, and without running any host code.
specs/laws.md asks for exactly three things under "static analysis":
the set and precedence of imports, the set of possible actions, and
bubbled up context holes. Each is one function below. They are cheap by
construction, because the language puts the identifying half of every
such construct in a static position: an import's name, an action's key,
an adaptation's prefix and a ctx(...) parameter's path are all
String in the AST, never expressions.
Each analysis comes in two depths. The shallow one reads a single program. The deep one follows imports through a library table, which is what a host actually wants — "which actions can this page dispatch?" is a question about a program and everything it imports — and is cycle-safe, returning what it could reach rather than looping.
Kept in lockstep with ../tramaj-hs/src/Tramaj/Analysis.hs.
#staticImportNames Source
staticImportNames :: Program -> Set StringEvery library this program imports directly.
#transitiveImportNames Source
transitiveImportNames :: Map String Program -> Program -> Set StringEvery library reachable from this program, directly or through another import. A name that is imported but missing from the table is still reported — an unresolvable dependency is exactly what a caller wants to hear about — and a cycle terminates instead of looping.
#staticActionKeys Source
staticActionKeys :: Program -> Set StringEvery action key this program can emit from its own AST.
Adaptation is applied rather than ignored:
adapt-actions(x, prefix("user:")) over a subtree whose keys are
{save, delete} contributes {user:save, user:delete}, using the very
same adaptKey the evaluator will. This is the payoff of restricting
adaptation to identity-or-prefix.
Keys living inside an imported library are not included — this
function sees one program. Use deepActionKeys to follow imports.
#deepActionKeys Source
deepActionKeys :: Map String Program -> Program -> Set StringEvery action key this program can emit, following imports.
An import contributes its library's own keys, adapted by any
adapt-actions wrapping it — so
adapt-actions(import("button", ...), prefix("deployment:")) over a
library emitting deploy yields deployment:deploy without either
program being run.
This is an over-approximation on purpose: it reports what the program
could emit, including keys under a Branch arm that a given context
will never select. A missing library contributes nothing, and a cycle
is cut.
#contextHoles Source
contextHoles :: Program -> Set (Array String)Every path this program declares as a hole with ctx(path) — the
values its own context must carry for its imports to be wired up.
These are reads from this program's $ctx, performed where each
import is written. What makes them worth a function of their own is
that the author marked them: contextReads finds every context read,
contextHoles finds the ones written as holes, and only the second is
a promise about what feeds an import.
#deepContextHoles Source
deepContextHoles :: Map String Program -> Program -> Set (Array String)Context holes of this program and of every library it imports — the "bubbled up" view: what the whole dependency tree declares as holes.
Note that a hole in an imported library is a hole in that library's context, which is the parameter object its importer hands it — not a path in this program's context. The paths are reported as written, and it is the import that wires the library up which decides where each is filled from.
#contextReads Source
contextReads :: Program -> Set (Array String)Every path this program reads out of its own context, however it is
written: $ctx.a.b and a ctx(a.b) import parameter both count.
For a library, this is the shape of the parameter object it expects,
since a library's $ctx is its parameters — which is what makes
unsuppliedParams possible.
A bare $ctx, with no path, reads the whole context and contributes
the empty path.
#unsuppliedParams Source
unsuppliedParams :: Map String Program -> Program -> Array (Tuple String (Set (Array String)))For each import in this program, the paths its library reads from its context that the import does not supply — the parameters that still have to be saturated by calling the import value before a field is read off it.
One entry per import site, in traversal order, so two imports of the
same library with different parameters are reported separately. An
import whose library is missing from the table contributes an empty
set: what it needs is unknowable, not nothing, and staticImportNames
is where an unresolvable dependency is reported.
Two deliberate imprecisions, both in the safe direction for a caller asking "what have I forgotten?":
- It over-reports, like
deepActionKeys: a path a library reads only in abrancharm that never fires still counts as needed. - It stops at the library's own reads and does not follow that library's imports, whose parameters that library supplies itself.
A read is attributed to a parameter by its first segment, so a library
reading $ctx.spec.replicas needs the parameter spec; the whole
path is reported, because the shape wanted is more useful than the
name alone. A bare $ctx read names no parameter and is skipped.
#constraintKinds Source
constraintKinds :: Program -> Set StringEvery constraint name this program can emit from its own AST
(v3-symbols §7). Answerable statically because a constraint(...)'s
name is a static string, like an action's event and key.
#deepConstraintKinds Source
deepConstraintKinds :: Map String Program -> Program -> Set StringEvery constraint name this program can emit, following imports — the
question a host actually wants answered before it decides whether it
supports a template (v3-symbols §7: "lets a host decide ... before
running it"). Over-approximates like deepActionKeys: a kind emitted
only under a Branch arm no context will select is still reported, and
a missing library contributes nothing rather than failing.
#symbolSites Source
symbolSites :: Program -> Set IntThe ?(k) allocation sites this program contains (v3-symbols §7) —
sites, not keys, since the number of symbols a site produces is a
runtime fact (one per map iteration, say) but the number of sites is
not.
This is also the static counterpart of AllocationInLibrary (§6): a
program with a non-empty symbolSites cannot serve as a library, which
is exactly the check Tramaj.Eval.runLibrary makes before evaluating
one.
#symbolDemands Source
symbolDemands :: Program -> Set (Array String)Every context path this program declares symbolic with ?ctx.… (§7),
directly — the demand form's counterpart of contextHoles.
#deepSymbolDemands Source
deepSymbolDemands :: Map String Program -> Program -> Set (Array String)Demands bubbled up through every library this program imports, the way
deepContextHoles bubbles up ctx(...) holes — the counterpart of
ref §9's unsuppliedParams for this feature (§7): since only the root
may allocate, knowing which paths the libraries beneath will discuss
symbolically is the whole planning problem, answered without running
anything.
#typeDeclarations Source
typeDeclarations :: Program -> Set StringEvery name this program declares with type ... = ... (v4-types §9) —
the type-realm counterpart of a program's own let-bound names, and the
source Tramaj.Types.resolveTypeExpr consults for a bare TName.
#typeParams Source
typeParams :: Program -> Set (Array String)Every %ctx.* path this program mentions in a type-bearing position
(v4-types §9, roadmap Phase 10) — its type-level parameter list, the
way contextReads is a program's value-level one. This is what a
library exposes for its importer to supply via a %-marked param
(§2), and it must include a forwarding import's own %ctx.* params,
not only what a type ... = ... declaration mentions directly:
forwarding (import("inner", {payload: %ctx.payload}), §2) is exactly
how a library that itself has no annotated declaration still has an
unsaturated parameter its own importer must close.
#unsuppliedTypeParams Source
unsuppliedTypeParams :: Map String Program -> Program -> Array (Tuple String (Set (Array String)))For each import in this program, the type params (typeParams) its
library needs that the import's %-marked entries do not supply —
unsuppliedParams' type-side twin (roadmap Phase 10), sharing the same
first-segment attribution rule and the same stopping condition: a read
is attributed to the parameter its path begins with, and a missing
library contributes nothing rather than failing.
#typeParamCollisions Source
typeParamCollisions :: Program -> Set StringParam keys this program reads both as an ordinary value hole
($ctx.k/ctx(k)) and as a type hole (%ctx.k) — v4-types §10's
TypeParamCollision, keyed by first segment exactly as
unsuppliedParams and unsuppliedTypeParams both are, since that
segment is the parameter name the two channels of §2.1's one params
record would otherwise share.
#typeExprsIn Source
typeExprsIn :: Expr -> Array TypeExprEvery TypeExpr sitting in one expression's own syntax, not recursing
into subexpressions — everywhere does that part. A declaration's
body, an annotation's type, a type constraint's type-marked arguments,
and an import parameter's %-marked value (v4-types §2, roadmap
Phase 10) are the four positions a TypeExpr can occur in at all.