arizona_eval (arizona v0.4.0)
View SourceEvaluates compile-time-emitted dynamics into runtime values.
Two flavors of evaluation:
- Plain (
eval_dynamics/1) -- evaluates dynamic closures into values, no view tracking and no dependency capture. Used for SSR-style first renders where there's no diff state to maintain. - Tracked (
eval_dynamics_v/2) -- evaluates dynamics through aViewsaccumulator, capturing per-dynamic dependencies via the process dictionary key$arizona_deps. Used during diff so the next render can fast-skip dynamics whose deps didn't change.
Each eval_one_v clause opens a dependency scope around its closure call
(open_deps/0 .. close_deps/1), so any arizona_template:get/2,3 calls
inside the closure populate that scope's map. The captured map becomes the
dynamic's Deps and is later compared against the Changed set in
arizona_diff:diff/4. A closure that raises closes its scope on the way out
the same way, so a render or diff run inside a try leaves no scope open.
Scopes nest: a scope opened while another is open hands its reads to the
enclosing one when it closes. That is how an ?each item's reads reach the
each slot -- items are evaluated inside the slot's scope, each item dynamic in
its own. A nested template, a ?stateless body and a ?stateful child's
lifecycle are evaluated under with_saved_deps/1 instead, which suspends the
enclosing scope: their reads stay out of it (the parse transform records the
ones that matter on the enclosing slot at compile time).
Container handling
Evaluation recurses into the following dynamic value shapes:
eachcontainers -- lists (source := [_]), streams (source := #stream{}), and maps (source := #{...})- Nested templates -- maps with
sandd - Stateful descriptors --
#{stateful := H, props := P}(mounts a child or reuses an existing one fromOldviews) - Stateless descriptors --
#{callback := F, props := P}
Restricted keys
check_restricted_keys/3,4 enforces that a handler's callbacks (mount and
every later one that returns bindings) cannot modify or drop framework-owned
bindings like id. Violations raise
restricted_key_modified with a human-readable message via format_error/2.
Summary
Functions
Asserts that Bindings did not modify any framework-restricted keys
(currently just id) compared to the original Props.
check_restricted_keys/3 for bindings returned by Callback rather than by
mount/1: Props is then what the callback was handed (a child's props, or
the bindings the view held before the callback ran), and the error names it.
Projects a views accumulator down to the child_views annotation stored on a
snapshot: the set of the view ids it rendered, with the view entries themselves
dropped (keeping them would pin every descendant's bindings and snapshot alive
inside its ancestors').
Claims the ids of the child views a pass keeps without evaluating them (a dep-skipped slot's subtree), for the duplicate view-id check.
Drains the update-effects accumulator after the render/diff just performed
(the seed plus every child handle_update/3's additions). The live process
calls this to ship them to the client. Returns [] if none accumulated.
Evaluates a list of dynamics into [{Az, Value}] snapshot entries.
Evaluates a list of dynamics with view tracking and dependency capture.
Re-evaluates a slot that last held an each container, under dep tracking, without rendering its items.
Runs Fun, which renders items of an ?each whose per-item template is
Template, in a dependency scope of its own, and returns {Result, ItemDeps}:
the binding keys the items read, at any depth (an inner each's items included).
Convenience wrapper around eval_one_v/2 that flattens the return shape
from {{Az, Val, Deps}, Views} to {Az, Val, Deps, Views}.
Evaluates a list of stream item keys against the items map and template,
threading view state. Returns a #{Key => ItemD} map and updated views.
Evaluates a dynamic whose snapshot Old holds an az-nodiff element, unless the
dynamic still produces that az-nodiff element.
Formats restricted_key_modified errors into a human-readable message naming
the offending handler, callback and key, and duplicate_view_id errors naming
the id and the two handlers that claimed it.
Re-raises an exception from a template dynamic tagged with the dynamic's source
location, as {arizona_loc, Loc, Reason}, keeping its class and stacktrace.
Renders list items with view tracking. Returns {[ItemD], Views1}.
Renders a single stream item by evaluating its dynamics with view tracking.
Returns {ItemD, Views1} where ItemD is [{Az, Value, Deps}].
Re-renders a stream item using Changed (set of fields differing between
old and new item) and the previous OldItemD, keeping the old
{Az, Val, Deps} triple of each per-item dynamic that cannot have changed, so
the downstream value comparison emits no op for it.
Returns the framework-owned binding keys. Mounted handlers cannot
change them (see check_restricted_keys/3); navigate-carry strips
them from the previous-route bindings before the new mount sees them,
since they're route-bound rather than session-level state.
Seeds the update-effects accumulator before a diff with the originating
callback's effects (e.g. the root event's). Child handle_update/3
callbacks fold onto this during the diff; drain_update_effects/0 reads
the final list. Threading the accumulator this way avoids concatenating
child effects onto the originating ones at the call site.
Functions
-spec check_restricted_keys(Bindings, Props, Handler) -> ok when Bindings :: map(), Props :: map(), Handler :: module().
Asserts that Bindings did not modify any framework-restricted keys
(currently just id) compared to the original Props.
Raises restricted_key_modified with handler info if a restricted key
was changed or dropped by the handler's mount callback.
-spec check_restricted_keys(Bindings, Props, Handler, Callback) -> ok when Bindings :: map(), Props :: map(), Handler :: module(), Callback :: mount | handle_update | handle_event | handle_info | handle_drain | default_patch_merge.
check_restricted_keys/3 for bindings returned by Callback rather than by
mount/1: Props is then what the callback was handed (a child's props, or
the bindings the view held before the callback ran), and the error names it.
-spec child_view_set(Views) -> ChildViews when Views :: #{binary() => term()}, ChildViews :: #{binary() => true}.
Projects a views accumulator down to the child_views annotation stored on a
snapshot: the set of the view ids it rendered, with the view entries themselves
dropped (keeping them would pin every descendant's bindings and snapshot alive
inside its ancestors').
A SET rather than a list, because every consumer asks only "is this id in
there?" -- so the annotation is on the small side of the membership test and the
prune in arizona_live's root-snapshot settle iterates the handful of ids on a
path to a change instead of scanning a page-sized child_views per container.
Nothing reads it in order.
Claims the ids of the child views a pass keeps without evaluating them (a dep-skipped slot's subtree), for the duplicate view-id check.
A live process keys its views by id and routes events, timers and messages by
it, so two views under one id collapse into one entry: one instance is never
unmounted, the other's slot is updated from its state, and everything addressed
to either reaches only one. arizona_live therefore arms the check around every
render or diff it runs, by storing
{RootBindings, RootHandler, Outside, #{}} under '$arizona_view_ids' and
erasing it after: every child view the pass evaluates (eval_stateful) or keeps
(this function) claims its id, and a second claim of one id, or a claim of the
root's own id, raises {duplicate_view_id, Id, FirstHandler, SecondHandler}.
Outside is none for a pass that renders the whole tree, or
{Views, ChildSnapshot} for one that renders a single child's subtree: every
view of Views outside that snapshot's child_views is mounted elsewhere on the
page and keeps its id. Arming is one process-dictionary write and everything else
is paid per claim, so a pass that evaluates and keeps no child view costs nothing
more. Unarmed (SSR, static generation) nothing is checked: there is no views map
to corrupt.
-spec drain_update_effects() -> [arizona_stateful:effect()].
Drains the update-effects accumulator after the render/diff just performed
(the seed plus every child handle_update/3's additions). The live process
calls this to ship them to the client. Returns [] if none accumulated.
-spec eval_dynamics(Dynamics) -> EvaledDynamics when Dynamics :: [arizona_template:dynamic()], EvaledDynamics :: [{arizona_template:az(), term()}].
Evaluates a list of dynamics into [{Az, Value}] snapshot entries.
No view tracking, no dependency capture. Used for SSR-style renders where there is no diff state to maintain.
-spec eval_dynamics_v(Dynamics, Views) -> {Triples, Views1} when Dynamics :: [arizona_template:dynamic()], Views :: {map(), map()}, Triples :: [{arizona_template:az(), term(), map()}], Views1 :: {map(), map()}.
Evaluates a list of dynamics with view tracking and dependency capture.
Returns {Triples, Views1} where each triple is {Az, Value, Deps}.
Deps is the set of binding keys read during evaluation, captured via
the $arizona_deps process dictionary entry.
-spec eval_each_def(Dynamic, Views) -> Result when Dynamic :: arizona_template:dynamic(), Views :: {map(), map()}, Result :: {each, arizona_template:az(), map(), map()} | {changed, arizona_template:az(), term(), map(), {map(), map()}}.
Re-evaluates a slot that last held an each container, under dep tracking, without rendering its items.
Answers {each, Az, EachValue, Deps} while the slot still produces an each
container (#{t := ?EACH, source, template}), for the each-specific diff to
render the items against the old snapshot; Deps are then only the reads of the
slot's own closure (the source, a branch condition), and the diff adds the
items' through eval_items/2. The slot need not still hold an each -- a
conditional whose branch was one can land on any value -- and anything else is
evaluated from the value already produced, answered
{changed, Az, Value, Deps, Views1} as eval_unless_same_nodiff/3 does.
-spec eval_items(Template, Fun) -> {Result, ItemDeps} when Template :: map(), Fun :: fun(() -> Result), Result :: term(), ItemDeps :: map().
Runs Fun, which renders items of an ?each whose per-item template is
Template, in a dependency scope of its own, and returns {Result, ItemDeps}:
the binding keys the items read, at any depth (an inner each's items included).
They also reach the enclosing scope, if one is open, so an each slot evaluated
with its items depends on whatever its items read -- a change to an outer
binding read inside the callback re-evaluates the each, and the per-item diff
then patches only the items it changed. An az-nodiff item template renders once,
so its reads are suspended rather than collected (#{}).
-spec eval_one_v_flat(Dynamic, Views) -> {Az, Value, Deps, Views1} when Dynamic :: arizona_template:dynamic(), Views :: {map(), map()}, Az :: arizona_template:az(), Value :: term(), Deps :: map(), Views1 :: {map(), map()}.
Convenience wrapper around eval_one_v/2 that flattens the return shape
from {{Az, Val, Deps}, Views} to {Az, Val, Deps, Views}.
-spec eval_stream_items(Keys, ItemsMap, Template, Views) -> {ItemSnaps, Views1} when Keys :: [term()], ItemsMap :: #{term() => term()}, Template :: map(), Views :: {map(), map()}, ItemSnaps :: #{term() => [{arizona_template:az(), term(), map()}]}, Views1 :: {map(), map()}.
Evaluates a list of stream item keys against the items map and template,
threading view state. Returns a #{Key => ItemD} map and updated views.
-spec eval_unless_same_nodiff(Dynamic, Old, Views) -> Result when Dynamic :: arizona_template:dynamic(), Old :: arizona_template:snapshot(), Views :: {map(), map()}, Result :: {same, map()} | {changed, arizona_template:az(), term(), map(), {map(), map()}}.
Evaluates a dynamic whose snapshot Old holds an az-nodiff element, unless the
dynamic still produces that az-nodiff element.
The slot's closure runs under dep tracking as in eval_one_v_flat/2, but its
result is inspected before anything inside it is evaluated (a ?stateless
descriptor is rendered to its template first): a template that is the same az-nodiff element
(arizona_template:same_nodiff/3) answers {same, Deps} and its dynamics never
run -- the az-nodiff element renders once, so evaluating them would only cost the whole
az-nodiff element per change. Anything else is evaluated from the value already produced
(the closure is not called twice) and answered {changed, Az, Value, Deps, Views1}.
-spec format_error(Reason, StackTrace) -> ErrorInfo when Reason :: term(), StackTrace :: [tuple()], ErrorInfo :: #{general := iolist()}.
Formats restricted_key_modified errors into a human-readable message naming
the offending handler, callback and key, and duplicate_view_id errors naming
the id and the two handlers that claimed it.
-spec relocate(Class, Reason, Stacktrace, Loc) -> no_return() when Class :: error | exit | throw, Reason :: term(), Stacktrace :: erlang:stacktrace(), Loc :: {module(), pos_integer()}.
Re-raises an exception from a template dynamic tagged with the dynamic's source
location, as {arizona_loc, Loc, Reason}, keeping its class and stacktrace.
Every evaluator of a located dynamic catches here -- the SSR walk, the live mount and the diff alike -- so a crash names the template line whichever path ran it. A reason already tagged by a nested dynamic is re-raised unchanged: the deepest location is the one closest to the failure.
-spec render_list_items(Items, Template, Views) -> {ItemDs, Views1} when Items :: [term()], Template :: map(), Views :: {map(), map()}, ItemDs :: [[{arizona_template:az(), term(), map()}]], Views1 :: {map(), map()}.
Renders list items with view tracking. Returns {[ItemD], Views1}.
-spec render_stream_item(Key, Item, Template, Views) -> {ItemD, Views1} when Key :: term(), Item :: term(), Template :: map(), Views :: {map(), map()}, ItemD :: [{arizona_template:az(), term(), map()}], Views1 :: {map(), map()}.
Renders a single stream item by evaluating its dynamics with view tracking.
Returns {ItemD, Views1} where ItemD is [{Az, Value, Deps}].
-spec render_stream_item_skipping(Key, NewItem, OldItemD, Changed, Template, Views) -> {ItemD, Views1} when Key :: term(), NewItem :: term(), OldItemD :: [{arizona_template:az(), term(), map()}], Changed :: map(), Template :: map(), Views :: {map(), map()}, ItemD :: [{arizona_template:az(), term(), map()}], Views1 :: {map(), map()}.
Re-renders a stream item using Changed (set of fields differing between
old and new item) and the previous OldItemD, keeping the old
{Az, Val, Deps} triple of each per-item dynamic that cannot have changed, so
the downstream value comparison emits no op for it.
A dynamic qualifies only when the parse transform flagged it in the template's
reuse list (every item read it makes is tracked in its own closure, so its
deps name every field it read) and its deps miss Changed. Any other dynamic
re-evaluates, as does every dynamic of a template without the list: a read the
deps never recorded is an input the item change may have touched.
-spec restricted_keys() -> [atom()].
Returns the framework-owned binding keys. Mounted handlers cannot
change them (see check_restricted_keys/3); navigate-carry strips
them from the previous-route bindings before the new mount sees them,
since they're route-bound rather than session-level state.
-spec set_update_effects(Effects) -> ok when Effects :: [arizona_stateful:effect()].
Seeds the update-effects accumulator before a diff with the originating
callback's effects (e.g. the root event's). Child handle_update/3
callbacks fold onto this during the diff; drain_update_effects/0 reads
the final list. Threading the accumulator this way avoids concatenating
child effects onto the originating ones at the call site.