StatifierBlocks.Environment (StatifierBlocks v0.20.0)

Copy Markdown View Source

What is known at a position: a map from datamodel path to type, carried through the document by a pre-order walk (ADR-0011 decision 1).

Nothing flows between adjacent blocks. Every value a block produces is written to a datamodel path by name and every value it reads is read from one, so the data-flow question at a position is a question about the paths the document has written on the way there - not about the block before it.

The type of a type

A type is one of the nine the datamodel document closes its set at, the name of a record or shape that document declares, an opaque string a host carries, {:list, type}, or :unknown. This module mints none of them: every one arrives from a block's own declaration or from the document. type_of/2 reads a declaration's spelling into StatifierDatamodel.Types.t/0 and satisfies/3 hands the pair to StatifierDatamodel.Types.satisfies/3, which is the read check (ADR-0011 decision 3). There is no second one here, and no Compatibility or Coverage module of this package's own.

The string "unknown" reads as :unknown rather than as an opaque string spelled that way, so that StatifierDatamodel.Types.to_string/1 and type_of/2 are inverse over the whole grammar. It is the one reinterpretation of an expression a host could already have been carrying, and it only ever admits: an opaque "unknown" compared by identity was already satisfied against another "unknown", so nothing that passed before is refused now.

The walk

Document.blocks/1's pre-order, carrying the environment forward. For a block reached with environment env:

  • each slot the block carries is walked from env - the shelf is not entered (ADR-0003's amendment of 2026-08-31, A2), and each parked fragment is walked from an empty environment instead;
  • what the slots produce is merged per path by agreement (decision 4): a path every slot holds at one type keeps it, a path some hold and others do not - or hold differently - drops to :unknown, and a path no slot holds is absent;
  • the block's own write signatures are applied to the merge.

A container with one slot merges to that slot's own answer, so a group body's writes leave the group. A core.on_event in a group's interrupt slot writes its captures on that arm alone, so a path only it holds leaves the group at :unknown - decision 10's answer, arrived at by decision 4 rather than by a special case.

Why the walk terminates

It descends. at/3 asks StatifierBlocks.Document.fetch_path/2 for the path from the root to the position it was given, and then walks down that path once, carrying the environment: each step folds the siblings before it and moves to a strictly deeper block, and the path is finite because the document is a finite tree. Nothing here asks an ancestor for its own position, so there is no recursion to bound and a document built of nothing but empty sequences costs one step per level - which is also why the answer stays cheap enough for the editor to compute on mousedown.

Signatures

A block declares what it reads and writes on its fields, not on itself (ADR-0011 decision 2), so a finding anchors on the field key the author has to change:

  • {:path, %{writes: T}} writes T at the path the field's value names;
  • a {:path, opts} field with no writes key, and a field carrying datamodel_path?: true, write :unknown there - the path becomes known without becoming typed;
  • a capture config map writes :unknown at each of its keys, one per pair;
  • {:path, %{expects: T}} reads T there.

io/1's consumes and produces are sugar over the subject path, which the entry block's palette entry names with subject: (decision 6): consumes is a read there and produces is a write there. A document whose entry block declares no subject has no subject path, and the sugar is inert. A produces of :unknown writes nothing rather than blanking the subject - a container that says nothing about the subject leaves it alone, which is the whole gain decision 4's per-path merge exists for.

Summary

Types

The environment with the block that wrote each entry, which is what ADR-0011 decision 8's upstream_ref names. :slot_entry is the seed, or a merge whose arms agreed on a type without agreeing on who put it there.

Caller-supplied, not stored in the document. :datamodel is the datamodel document the declarations are read from; :entry_type seeds the subject path for a document whose entry block declares no produces of its own.

One declared read or write: the field key it is declared on (or :consumes / :produces for the sugar, and :capture for a capture pair), the path it names, and the type.

t()

Datamodel path to type. ADR-0011 decision 1's environment.

A position, as ADR-0001 decision 5 defines one.

A type at a path. One of StatifierDatamodel.Types.t/0's inhabitants as a document spells it - a scalar name, a declared name, an opaque string - or :unknown, or a list of one of those.

Functions

at/3, keeping the block that wrote each entry.

The environment at target, as a map from datamodel path to type.

The declarations ctx[:datamodel] carries, or an empty index.

Every read block declares, in config_schema/1 order with the consumes sugar last.

The read check: StatifierDatamodel.Types.satisfies/3 over the two spellings, and nothing else (ADR-0011 decision 3).

The environment the document opens with (ADR-0011 decision 2).

The datamodel path the document's subject lives at, or nil.

How a type expression is written for a human (ADR-0011 decision 9).

Reads a declared spelling into a type expression.

Every write block declares, in config_schema/1 order, then its capture pairs, then the produces sugar (ADR-0011 decision 2).

Types

annotated()

@type annotated() :: %{
  optional(String.t()) =>
    {type_expr(), StatifierBlocks.Block.id() | :slot_entry}
}

The environment with the block that wrote each entry, which is what ADR-0011 decision 8's upstream_ref names. :slot_entry is the seed, or a merge whose arms agreed on a type without agreeing on who put it there.

context()

@type context() :: %{
  optional(:entry_type) => type_expr(),
  optional(:datamodel) => term()
}

Caller-supplied, not stored in the document. :datamodel is the datamodel document the declarations are read from; :entry_type seeds the subject path for a document whose entry block declares no produces of its own.

signature()

@type signature() :: {key :: String.t() | atom(), path :: String.t(), type_expr()}

One declared read or write: the field key it is declared on (or :consumes / :produces for the sugar, and :capture for a capture pair), the path it names, and the type.

t()

@type t() :: %{optional(String.t()) => type_expr()}

Datamodel path to type. ADR-0011 decision 1's environment.

target()

A position, as ADR-0001 decision 5 defines one.

type_expr()

@type type_expr() :: String.t() | :unknown | {:list, type_expr()}

A type at a path. One of StatifierDatamodel.Types.t/0's inhabitants as a document spells it - a scalar name, a declared name, an opaque string - or :unknown, or a list of one of those.

Functions

annotated(palette, document, target, ctx \\ %{})

at/3, keeping the block that wrote each entry.

ADR-0011 decision 8's {:type_mismatch, ...} names the block whose write signature the read disagrees with, and this is where that name comes from.

at(palette, document, target, ctx \\ %{})

The environment at target, as a map from datamodel path to type.

target is a position {parent_id, slot, index}: the answer is what the block at that index sees, before its own writes are applied. Total - a parent no block carries, or an index past the end of a slot, answers with whatever the walk reached, never a raise.

declarations(ctx)

@spec declarations(context()) :: StatifierDatamodel.Declarations.t()

The declarations ctx[:datamodel] carries, or an empty index.

StatifierDatamodel.Declarations.from_document/1 is total: a datamodel that is not a document, or one with no types key, declares nothing rather than failing, and a walk over it produces :unknown and not an exception.

read_signatures(palette, document, block)

Every read block declares, in config_schema/1 order with the consumes sugar last.

A block with three path fields declares three reads and they are independent (ADR-0011 decision 2).

satisfies(declarations, held, expected)

The read check: StatifierDatamodel.Types.satisfies/3 over the two spellings, and nothing else (ADR-0011 decision 3).

The palette's host relation is not asked here. It runs last, after this returns not-satisfied, in StatifierBlocks.Assignability.assignable?/4.

seed(palette, document, ctx \\ %{})

The environment the document opens with (ADR-0011 decision 2).

ctx[:entry_type] at the subject path, and nothing else. It is ADR-0003 decision 4's context key, kept meaning what it meant - the type entering the document - now that the document has a path to hold it at. A document with no entry block, or one whose palette entry declares no subject:, seeds empty however the context is filled: with no subject path there is nowhere for a subject type to be. Every read is then a read of a path the environment does not hold, which is an advisory and not an error, so an untyped document validates exactly as it did.

The entry block's own writes are not applied here. Decision 2 says the document opens with its subject path holding its subject type, and the walk is what puts it there: the entry block is the first position walked, so every position after it sees exactly that. Applying its writes ahead of the walk would additionally put them in front of the entry block's own reads, and decision 1 is explicit that a block's reads are checked before its own writes are applied - a block does not read what it is about to write.

subject_path(palette, document)

@spec subject_path(StatifierBlocks.Palette.t(), StatifierBlocks.Document.t()) ::
  String.t() | nil

The datamodel path the document's subject lives at, or nil.

ADR-0011 decision 6: the subject: key on the entry block's palette_entry/0 - the first block of the root's body slot. nil means the document has no subject, and consumes and produces desugar to nothing at all.

type_label(declarations, spelling)

@spec type_label(StatifierDatamodel.Declarations.t(), term()) :: String.t()

How a type expression is written for a human (ADR-0011 decision 9).

A spelling that names a declaration in declarations renders that declaration's label, which is the human-readable name the record asks a finding and the Datamodel tab to carry so an author reads "Credit card transaction" instead of a nominal name they have to go and look up. Every other spelling renders exactly as it did before there were declarations: one of the nine scalars as its own word, an opaque string a host carries as itself, :unknown as unknown, and a list as what it holds.

It is a rendering and nothing else. No verdict reads it, nothing branches on it, and a declaration whose label is absent renders its name

  • so a document that declares types without labelling them shows exactly what it showed before, rather than a blank where a name used to be.

    iex> alias StatifierBlocks.Environment iex> declarations = StatifierDatamodel.Declarations.from_document(%{"types" => [ ...> %{"name" => "cards.credit_txn", "kind" => "record", ...> "label" => "Credit card transaction", "fields" => []}]}) iex> Environment.type_label(declarations, "cards.credit_txn") "Credit card transaction" iex> Environment.type_label(declarations, "myapp.card_txn") "myapp.card_txn" iex> Environment.type_label(declarations, :unknown) "unknown" iex> Environment.type_label(declarations, {:list, "cards.credit_txn"}) "list of Credit card transaction"

type_of(declarations, spelling)

Reads a declared spelling into a type expression.

Total, and it defers to StatifierDatamodel.Types.parse/2 for everything the grammar already covers. Two readings are this package's own: the atom :unknown and the string "unknown" are both :unknown, and a {:list, _} is the document's own list - ADR-0011 decision 14 puts no cardinality on a read, so a list is checked as a list and its item type is carried for a fan-out to bind, not for the check to descend into.

write_signatures(palette, document, block)

Every write block declares, in config_schema/1 order, then its capture pairs, then the produces sugar (ADR-0011 decision 2).