arizona_eval (arizona v0.4.0)

View Source

Evaluates 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 a Views accumulator, 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:

  • each containers -- lists (source := [_]), streams (source := #stream{}), and maps (source := #{...})
  • Nested templates -- maps with s and d
  • Stateful descriptors -- #{stateful := H, props := P} (mounts a child or reuses an existing one from Old views)
  • 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

check_restricted_keys(Bindings, Props, Handler)

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

check_restricted_keys(Bindings, Props, Handler, 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.

child_view_set(Views)

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

claim_view_ids(Views)

-spec claim_view_ids(Views) -> ok when Views :: #{binary() => #{handler := module(), _ => _}}.

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.

drain_update_effects()

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

eval_dynamics(Dynamics)

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

eval_dynamics_v(Dynamics, Views)

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

eval_each_def(Dynamic, Views)

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

eval_items(Template, Fun)

-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 (#{}).

eval_one_v_flat(Dynamic, Views)

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

eval_stream_items(Keys, ItemsMap, Template, 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.

eval_unless_same_nodiff(Dynamic, Old, 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}.

format_error(Reason, StackTrace)

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

relocate(Class, Reason, Stacktrace, Loc)

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

render_list_items(Items, Template, Views)

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

render_map_items/3

render_stream_item(Key, Item, Template, Views)

-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}].

render_stream_item_skipping(Key, NewItem, OldItemD, Changed, Template, Views)

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

restricted_keys()

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

set_update_effects(Effects)

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