Files
tricu/docs/view-contracts.md

17 KiB

View Contracts and View Trees

1. Purpose

View Contracts are the portable checking layer for Tree Calculus programs.

The checker does not consume detached metadata about a separate executable. Its canonical input is a typed, checkable tree artifact: ordinary tree data that contains both the executable program payloads and the view/contract structure needed to validate and transform them.

The checker consumes this artifact and returns either:

checked-execution artifact

or:

structured diagnostic

A checked-execution artifact is interpreted by runChecked. Unguarded programs are represented as checkedPure rootPayload; guarded programs contain explicit checked guard/bind nodes.

This keeps checking independent of any particular host implementation. A typed artifact may be produced by any frontend, compiler, hand-written generator, or future self-hosted tricu toolchain.

2. Design Principle

The model follows the same discipline as interaction trees.

Interaction trees use tagged structural envelopes with explicit executable payloads:

io action      = pair "tricuIO" (pair version action)
pure x         = pair 0 x
bind action k  = pair 1 (pair action k)

The interpreter understands the outer structure, but it does not recursively mistake every subtree for interpreter metadata. A continuation k is an opaque executable tree until the interpreter reaches the bind step that applies it.

View trees use the same rule:

structure says how to check;
opaque executable fields are only executed/applied by the checker at the
appropriate step.

This is the key distinction that allows Views to carry guards without confusing ordinary program trees with View metadata.

3. Views

A View is an extrinsic contract over an ordinary Tree Calculus value. Tree Calculus values do not carry native runtime types; a View describes how a value may be treated by the checker or by a checked boundary.

Core View forms:

Any
Ref ref
Fn [argView...] resultView
List elemView
Maybe elemView
Pair leftView rightView
Result errView okView
Guarded baseView guard

Ref supports both generated/numeric and symbolic references. Symbolic refs are preferred for user-authored views:

UserId = viewRef "UserId"

A guarded view refines a base view with an executable guard:

UserId = viewGuarded (viewRef "UserId") userIdGuard

The guard is ordinary program code. The View validator checks that the guarded view envelope is well-formed, and recursively validates the baseView, but it must treat the guard payload/reference as opaque executable data, not as another View.

4. Polymorphic and Abstract Views

View Contracts support portable polymorphism over Views. The View language is interpreted by the same portable checker model implemented in tricu terms.

Source syntax may use underscore-prefixed names as View variables inside annotations:

id x@_a =@_a x
const x@_a y@_b =@_a x
compose f@(Fn [_b] _c) g@(Fn [_a] _b) x@_a =@_c f (g x)

In the portable artifact, these lower to scoped View binders rather than unscoped source-name conventions. This fits the existing View encoding style: Views are tagged records with numeric tags and tagged fields. Polymorphic forms are View records such as:

Var localId
Forall binders body
Exists binders body

The current durable encoding uses stable local binder IDs. For example, id x@_a =@_a x exports a shape equivalent to:

Forall [0] (Fn [Var 0] (Var 0))

Source names like _a are for authoring; the artifact carries binder scope and local IDs rather than relying on source-name identity.

Forall supports generic contracts:

map f@(Fn [_a] _b) xs@(List _a) =@(List _b) ...
head xs@(NonEmptyList _a) =@_a ...

At each checked use, the checker instantiates quantified variables into use-local internal variables and solves View compatibility constraints. The portable checker uses structural use-local IDs rather than expensive numeric freshening, and treats unconstrained variable-variable matches as constraints that do not create substitution cycles. Concrete observations still bind these variables when enough information is available. This is what lets explicitly annotated higher-order boundaries accept polymorphic values, for example compose id id "x", and lets quantified values satisfy concrete requirements such as Fn [String] String. It gives useful polymorphic contracts for explicitly declared/imported View facts.

Exists supports checked abstraction boundaries. A module can expose a value as "some representation _repr plus capabilities over _repr":

Exists _repr.
  Pair
    (Fn [String] _repr)   -- constructor
    (Fn [_repr] String)   -- renderer / eliminator

This does not make raw Tree Calculus inspection impossible. Unchecked code can always inspect trees. It means checked clients cannot justify representation-specific operations through the View system unless the package exports an appropriate capability or eliminator.

This leads to an important distinction for future checked subsets:

controlled observation: Bool/List/Maybe/Result/etc. eliminators with Views
raw observation: direct tree-shape inspection through triage-like power

Useful application code can live mostly in the controlled fragment and receive explicit View validation over lambdas, application, let, and typed eliminators. Low-level library code may still use raw intensionality, but should expose disciplined Views and capabilities above it. Scott-encoded constructors and eliminators are a natural tricu-native representation for these APIs.

Tree Calculus terms do not carry intrinsic principal Views, and raw intensional code can invalidate parametric claims. View Contracts are an explicit evidence and contract layer over tricu programs; limited polymorphic Views are supported when they are declared or imported as facts with provenance.

The first stdlib annotation island starts with parametric functions that do not inspect representation:

id x@_a =@_a x
const x@_a y@_b =@_a x
compose f@(Fn [_b] _c) g@(Fn [_a] _b) x@_a =@_c f (g x)

Re-export-only modules preserve imported View metadata, so these contracts flow through prelude rather than only through direct base imports.

Functions built on raw t/triage should enter the checked world through trusted, controlled eliminator contracts rather than by treating arbitrary raw inspection as parametric.

5. Guards

Guards are ordinary tricu values/functions grouped with the Views they refine.

Example:

userIdGuard = value :
  -- ordinary program that validates value

UserId = viewGuarded (viewRef "UserId") userIdGuard

loadUser id@UserId = ...

Guards return the standard checked-runtime protocol:

guardOk value
guardFail

Guards do not author diagnostics. The checked-exec runner owns guard failure and malformed-guard diagnostics using boundary context from the checked artifact.

Guards are injected by the checker. They are not discovered by the runtime as a separate metadata layer. The checking process transforms a view tree into an executable tree with the necessary guard applications inserted.

6. View Tree Artifact

The primary checker-facing artifact is a view executable term graph.

Conceptually:

ViewTree
  version
  root node id
  nodes

Each node is tagged tree data. Nodes combine executable payloads, view claims, and structural relationships in one graph.

Representative node forms:

Value node view executableTree
Apply node calleeNode argNode expectedOrInferredView
Require node requiredView sourceNode
External node name view

This is not a mandatory final encoding; it is the semantic target. The important property is that executable trees and checking structure are carried together in a single portable artifact.

A node may contain opaque executable fields. Those fields are tree terms, but they are not recursively decoded as view-tree nodes or Views unless the node's semantics explicitly says so.

View facts may also carry explicit per-fact trust provenance:

Checked    -- derived by checked lowering / checker validation
Trusted    -- asserted by a trusted boundary, e.g. a primitive eliminator API
Unchecked  -- raw or assumed; no parametricity/abstraction guarantee

In the portable view-tree envelope this provenance is represented as an optional field on typedValue / typedRequire facts. In module manifests the same provenance is carried beside the exported View Contract object reference so that imports and re-exports preserve it without relying on module-level convention. Absent provenance is interpreted conservatively as Unchecked at use sites.

For parametric checked definitions, the frontend now performs a conservative raw-intensionality dependency pass over local definitions. If a definition with scoped View variables depends directly or indirectly on raw triage / raw t construction, or on an imported Unchecked fact, lowering fails and asks the author to route observation through a trusted eliminator boundary. This is intentionally provenance/dependency based; it is not an attempt to decide whether arbitrary Tree Calculus reduction will ever reach rule 3.

View facts can be authored as ordinary value-level Tree Calculus metadata under one conventional top-level name:

viewFacts = [fact ...]
fact      = pair exportName (pair provenance view)

where exportName is a string naming a value exported by the module, provenance is 0 = Checked, 1 = Trusted, or 2 = Unchecked, and view is the same portable View record used by view-tree artifacts. The host evaluates this value and decodes the data schema; it does not infer trust from source syntax, AST shape, module name, or a Haskell-side catalog.

The initial trusted eliminator facts are authored this way in clearly separated stdlib viewFacts sections:

matchBool  : forall r. r -> r -> Bool -> r
matchMaybe : forall a r. r -> (a -> r) -> Maybe a -> r
matchList  : forall a r. r -> (a -> List a -> r) -> List a -> r

The base module provides small facts* authoring helpers for this advanced metadata, e.g. factsFact, factsChecked, factsTrusted, factsUnchecked, factsForall, factsFn, factsVar, factsBool, factsString, factsByte, factsUnit, factsMaybe, and factsList. These helpers construct ordinary Tree data; authority comes from the exported viewFacts value and its explicit provenance tags. Loader validation rejects duplicate fact names and facts for names the module does not export.

Initial derived stdlib annotations using this trusted kernel include:

maybeMap  : forall a b. (a -> b) -> Maybe a -> Maybe b
maybeBind : forall a b. Maybe a -> (a -> Maybe b) -> Maybe b
maybeOr   : forall a. a -> Maybe a -> a

Recursive list combinators are currently published as explicit Trusted value-level facts rather than Checked source annotations, because their bodies pass through raw fixed-point machinery that the conservative parametric taint pass intentionally does not prove safe. This is the stabilized boundary: raw stdlib kernels establish conventions with explicit authority; ordinary checked clients consume those facts rather than re-proving the internals.

headMaybe / lastMaybe / nthMaybe
append / map / filter / foldl / foldr
length / reverse / snoc / count / all? / any? / intersect
take / drop / splitAt / concatMap / find / partition / zipWith
string/list-byte helpers such as strLength, startsWith?, lines, words

7. Checker Semantics

The checker is an interpreter over the view tree.

For each node it may:

  1. validate the node envelope;
  2. validate Views referenced by the node;
  3. check compatibility between expected and actual Views;
  4. recursively check child nodes;
  5. inject guards required by guarded Views;
  6. produce the executable tree for that node;
  7. memoize node results by node id.

The root node result is a checked-execution program.

In abstract form:

checkViewTree : ViewTree -> Result CheckedExec Diagnostic

or, in self-hosted terms:

checkViewTree viewTree = ... -- ok checkedExec / err diagnostic

8. Compatibility and Guard Injection

Structural compatibility is about Views. Guard injection is about producing the checked-execution tree.

For example, if a node is required to satisfy:

viewGuarded (viewRef "UserId") userIdGuard

then the checker verifies the underlying View relationship and emits executable code that applies userIdGuard at the appropriate checked boundary.

The checker, not the runtime metadata system, owns this transformation.

9. Source Annotations

Source annotations are one frontend syntax for producing view-tree nodes.

Examples:

Nat = viewRef "Nat"
Box a = viewPair (viewRef "Box") a

idNat x@Nat =@Nat x
idBox x@(Box String) =@(Box String) x

Annotations are value-level View expressions. Names such as Nat and Box are ordinary program values/functions that evaluate to Views.

A frontend that supports this syntax should lower the source into a view tree that contains the relevant executable terms, views, and checking structure. The artifact must not depend on source names or on the frontend implementation that produced it.

10. Contract Expressions

Contract-expression helpers remain useful as authoring/building tools, but they are not the fundamental artifact model.

Preferred style for expression-oriented authoring is pipeline-first:

mapBoolStringUse = cFn <|
    [(viewFn [(viewBool)] viewString) (viewList viewBool)] (viewList viewString)
    |> cApply (cFn [(viewBool)] viewString)
    |> cApply (cValue (viewList viewBool))
    |> cRequire (viewList viewString)

These helpers should be understood as convenient ways to build typed/checkable structure, not as a permanent replacement for view-tree artifacts.

11. Artifact Direction

The target direction is to make the view tree the canonical checked-program artifact.

Older split concepts remain useful internally or during development:

tree term
view value
typed-program node
module/export manifest

But the durable design should avoid treating contracts as detached facts about a separate program. The portable checker input is the checkable program itself.

In short:

Do not store code over here and contracts over there.
Store a view tree: executable code plus the structure needed to check and guard it.

12. IO Interaction Trees

tricu IO is represented as ordinary interaction-tree data:

io action      = pair "tricuIO" (pair version action)
pure value     = pair 0 value
bind action k  = pair 1 (pair action k)

View Contracts do not change that representation. A checked program may produce an ordinary IO interaction tree, and the existing IO driver can execute it unchanged.

For source evaluation with contracts enabled, tricu eval --io performs an additional frontend instrumentation pass over visible IO continuations. When a continuation returns a pure (...) value that mentions source-annotated functions, the frontend lowers that pure expression into the existing portable checked-exec protocol before returning the next IO action.

This means source sugar works for practical checked IO paths such as:

acceptNames xs@(NonEmptyList String) =@String "accepted"

main = io (bind (pure []) (xs : pure (acceptNames xs)))

and for explicit higher-order boundaries:

useHandler handler@(Fn [(NonEmptyList String)] String) xs@(List String) =@String
  handler xs

main = io (bind (pure []) (xs : pure (useHandler acceptNames xs)))

The IO runtime does not perform View inference or guard injection at every step. The source/frontend pass constructs checked-exec boundaries once; the runtime only evaluates the resulting interaction tree.

Current limitations:

  • This is source-visible instrumentation, not whole-program function-flow tracking.
  • Higher-order guarantees require explicit annotated boundaries.
  • Raw prebuilt interaction trees, imported executable artifacts, and content-store terms are not automatically re-instrumented unless they pass through this source-lowering path.
  • The IO action shape itself is only shallowly checkable unless users provide guarded Views for the relevant boundaries.
  • Continuation result Views are not inferred from external effects; dynamic IO values should cross annotated/guarded boundaries when runtime enforcement is required.

Making IO checking more complete is future work. In particular, a future design may validate every continuation-produced action structurally, carry checked wrappers with higher-order function values, or define a portable checked-IO artifact instead of relying on Haskell/frontend source instrumentation.

13. Host Independence

No part of the core View Tree design is specific to Haskell or to the current implementation.

Any producer may emit a view-tree artifact if it follows the portable tree-data encoding. Any checker implementation may consume it if it implements the typed node semantics.

The current implementation can produce and consume these artifacts, but it is not the semantic authority. The artifact format and the self-hosted checker semantics are the authority.