11 KiB
Guard Injection Semantics
This document describes the runtime guard model for View Contracts.
Views describe portable structural contracts. Guarded views refine those
contracts with executable predicates while keeping ordinary value-level code free
of Maybe, Result, sentinel, or host-language abort handling.
viewGuarded baseView guard
A guarded view means: when this guarded view is observed along the reachable
checked-execution path, run guard against the runtime value.
Goals
- Preserve ordinary value-level program shapes.
- Keep guard failure out of user code.
- Avoid Haskell-specific checker/runtime semantics.
- Represent guard boundaries explicitly in portable tree data.
- Make successful guarded execution transparent: guarded values are unwrapped before ordinary code receives them.
- Prefer correctness-by-default over avoiding repeated predicate cost.
Non-goals
- Preventing user-written guards from diverging.
- Letting guards author their own diagnostics.
- Solving IO interaction-tree composition.
- Finalizing long-term artifact identity policy.
- Deduplicating or hoisting repeated guard checks.
Plain Views vs Guards
Plain Views still provide concrete benefits without guards:
- structural flow checking;
- portable API metadata;
- module/export contract metadata;
- content-store view-tree metadata;
- cross-frontend agreement on contract structure;
- diagnostics for wrong-view flows.
Guards are for invariants that require runtime value inspection, such as:
- non-empty list;
- sorted list;
- byte string of exactly 32 bytes;
- protocol payload with a valid checksum;
- domain-specific runtime predicate.
Guards are deliberately more expensive than ordinary Views. Use them when the runtime contract must be enforced.
Guard Result Protocol
Guards return one of two standardized shapes:
guardOk value
guardFail
Guards do not provide diagnostics. The checked-exec runner owns diagnostics. Malformed guard output is treated as a checked-runtime failure.
Checked Execution Protocol
A successful typed-program check returns a checked-execution artifact, not a raw payload.
Current constructors:
checkedPure value
checkedFail diagnostic
checkedGuard view guard value continuation
checkedGuardWithContext context view guard value continuation
checkedBind exec continuation
checkedGuard is the compatibility/default constructor. It lowers to
checkedGuardWithContext with an unknown context. Checker-injected guard
boundaries use checkedGuardWithContext so failures can identify where the
boundary came from.
Runner:
runChecked checkedExec
Semantics:
runChecked (checkedPure value)
= checkedRuntimeOk value
runChecked (checkedFail diagnostic)
= checkedRuntimeFail diagnostic
runChecked (checkedGuardWithContext context view guard value continuation)
= case guard value of
guardOk checkedValue -> runChecked (continuation checkedValue)
guardFail -> checkedRuntimeFail (guardFailed context view)
malformed -> checkedRuntimeFail (malformedGuardResult context view malformed)
runChecked (checkedGuard view guard value continuation)
= runChecked (checkedGuardWithContext unknownContext view guard value continuation)
runChecked (checkedBind exec continuation)
= case runChecked exec of
checkedRuntimeOk value -> runChecked (continuation value)
checkedRuntimeFail diag -> checkedRuntimeFail diag
Important invariant:
Guard failure is consumed by
runChecked. It is never passed into ordinary user code.
Checker Result Shape
checkTypedProgramWith returns checked-exec on success:
ok checkedExec env
Even unguarded programs return:
checkedPure rootPayload
Compatibility helper:
checkedProgramTree result
checkedProgramTree runs/unwraps checked-exec to preserve older raw-tree helper
behavior.
The Haskell tricu check path now evaluates successful checker output through
runChecked, so source-level guarded annotations fail through the same portable
checked-exec protocol.
Boundary Semantics
Guard insertion follows correctness-first semantics:
Every guarded View observation on the reachable checked-execution path runs its guard.
Important boundary kinds:
Guarded typed value
typedValue sym (viewGuarded base guard) payload
This observes sym as a guarded value. It also supplies base-view evidence for
flow checking.
Guarded requirement
typedRequire sym (viewGuarded base guard) payload
The symbol must satisfy base; the guarded observation is attached to sym and
is enforced when sym is used or exposed along the reachable root path.
Guarded function argument
For:
viewFn [(viewGuarded base guard)] result
application checking guards the argument before the callee receives it.
Guarded function result
For:
viewFn [arg] (viewGuarded base guard)
application checking guards the application result before exposing it as the result value.
Guarded callee symbol
If a function symbol itself has a guarded observation, that guard runs before the function value is applied. A successful guard may transform the function value; the application uses the guarded value.
Global Symbol Observations
Guarded typedValue and typedRequire nodes are global per-symbol
observations, not position-sensitive flow events.
All guarded observations for a symbol compose in typed-node order whenever that symbol is used or exposed on the reachable checked-execution path.
This means a later requirement still applies to an earlier syntactic use:
typedValue 1 viewString "x"
typedApply 2 f 1 "x"
typedRequire 1 (viewGuarded viewString guard) "x"
The guarded requirement is attached to symbol 1; compiling the reachable root
path that uses symbol 1 runs that guard.
Rationale:
- typed programs are declarative symbol graphs, not imperative event traces;
- global observations are simpler and more correct-by-default;
- producers cannot accidentally bypass a guard by ordering a requirement too late;
- staged raw/checked phases should use distinct symbols.
Reachability and Repetition
Guards are not run eagerly for every guarded node in a program.
Execution is root-reachable:
compileSymbol (typedProgramRoot program)
Only guarded observations reachable from the root checked-execution path run. Unreachable guarded symbols do not pay guard cost and do not fail execution.
Repeated reachable uses rerun guards. There is currently no deduplication or hoisting. This is intentional: each guarded observation/use is a runtime contract boundary.
Future optimization policies may add explicit deduplication or hoisting, but the baseline semantics are repeated, deterministic guard execution.
Function and Application Compilation
Checked execution is built compositionally from typed-node dependencies:
- compile the callee symbol;
- compile the argument symbol;
- run any guarded observations attached to the argument symbol;
- run the guarded function-argument boundary, if present;
- apply the callee to the checked argument;
- run the guarded function-result boundary, if present;
- run guarded observations attached to the application result symbol.
This handles nested and curried application chains because each typedApply
consumes one function argument and produces a symbol whose inferred view is the
function residual/result view.
Diagnostics
Guards do not author diagnostics. The checked-exec runner renders diagnostics from checker-owned boundary context plus the guarded View.
Checker-injected guard nodes carry portable structural context. Current context kinds are:
- root
typedValueexposure; - root
typedRequireexposure; - non-root
typedValuesymbol observation; - non-root
typedRequiresymbol observation; - function argument boundary;
- function result boundary;
- unknown/default context for manually constructed
checkedGuardvalues.
Examples:
guard failed at root typedValue symbol 0 for Guarded String
guard failed at root typedRequire symbol 3 for Guarded String
guard failed at typedRequire symbol 6 for Guarded String
guard failed at argument 0 of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
guard failed at result of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
malformed guard result at argument 0 of application symbol 2 (callee symbol 0, arg symbol 1) for Guarded String
Manually constructed checkedGuard values use unknown context and therefore
render without a boundary suffix:
guard failed for String
malformed guard result for String
The context is diagnostic-only. It does not affect guard execution, View compatibility, success/failure semantics, or continuation values.
The context deliberately contains raw portable data such as symbols and
application edges. It does not preserve source aliases such as NonEmptyString,
and it does not rely on Haskell-side post-processing or source-name annotation.
Named View rendering is a separate future design topic.
Why Not Abort in Haskell?
A host-level abort primitive would move guard semantics into Haskell. The design
instead encodes guard failure in portable checked-exec artifacts and interprets
it with portable tricu code.
Haskell may evaluate the runner, but Haskell is not the semantic source of guard validity or failure behavior.
Why Not Maybe / Result Everywhere?
Returning Maybe or Result from every guarded boundary would infect ordinary
APIs. A function expecting a List Byte would have to accept
Maybe (List Byte) or Result Error (List Byte), and every downstream caller
would need defensive handling.
The checked-exec runner avoids this. It unwraps successful guard results before continuing and stops checked execution on failure.
Known Sharp Edges
Guard divergence
A user-written guard may diverge. This design handles intentional failure via
guardFail; it does not solve arbitrary nontermination. Fuel or timeouts are
separate runtime concerns.
Payload trust
Typed nodes carry executable payloads. Guard injection must not expose an unchecked precomputed payload at a guarded boundary. Boundaries are mediated by checked-exec nodes.
This does not make malicious producer forgery impossible; it gives honest frontends a portable, checkable protocol that avoids accidental bypasses.
Cyclic typed-apply graphs
The current symbol compiler assumes typed programs are well-founded dependency graphs as emitted by the frontend/lowering path. Cyclic typed-apply graphs are a malformed-program validation concern, not a guard-specific semantic feature.
Current Implementation Status
Implemented in lib/view.tri and exercised by tests:
guardOk/guardFail;checkedPure,checkedFail,checkedGuard,checkedGuardWithContext,checkedBind;runChecked;- success from
checkTypedProgramWithreturns checked-exec; checkedProgramTreecompatibility helper;- guarded root exposure;
- guarded
typedValueandtypedRequire; - guarded function arguments and results;
- guarded callee observations;
- nested/curried application guard composition;
- global per-symbol observations;
- root-reachability behavior;
- repeated reachable uses rerun guards;
- source/Haskell
tricu checkintegration; - imported/module
VTGuardedlowering to portableviewGuarded; - portable guard boundary diagnostics with symbol/application context.