Tricu 2.0.0
Sorry for squashing all of this but 🤷
This commit is contained in:
371
docs/guard-injection.md
Normal file
371
docs/guard-injection.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# 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.
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
runChecked checkedExec
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```tri
|
||||
ok checkedExec env
|
||||
```
|
||||
|
||||
Even unguarded programs return:
|
||||
|
||||
```tri
|
||||
checkedPure rootPayload
|
||||
```
|
||||
|
||||
Compatibility helper:
|
||||
|
||||
```tri
|
||||
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
|
||||
|
||||
```tri
|
||||
typedValue sym (viewGuarded base guard) payload
|
||||
```
|
||||
|
||||
This observes `sym` as a guarded value. It also supplies base-view evidence for
|
||||
flow checking.
|
||||
|
||||
### Guarded requirement
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
viewFn [(viewGuarded base guard)] result
|
||||
```
|
||||
|
||||
application checking guards the argument before the callee receives it.
|
||||
|
||||
### Guarded function result
|
||||
|
||||
For:
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
```tri
|
||||
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:
|
||||
|
||||
1. compile the callee symbol;
|
||||
2. compile the argument symbol;
|
||||
3. run any guarded observations attached to the argument symbol;
|
||||
4. run the guarded function-argument boundary, if present;
|
||||
5. apply the callee to the checked argument;
|
||||
6. run the guarded function-result boundary, if present;
|
||||
7. 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 `typedValue` exposure;
|
||||
- root `typedRequire` exposure;
|
||||
- non-root `typedValue` symbol observation;
|
||||
- non-root `typedRequire` symbol observation;
|
||||
- function argument boundary;
|
||||
- function result boundary;
|
||||
- unknown/default context for manually constructed `checkedGuard` values.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```text
|
||||
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 `checkTypedProgramWith` returns checked-exec;
|
||||
- `checkedProgramTree` compatibility helper;
|
||||
- guarded root exposure;
|
||||
- guarded `typedValue` and `typedRequire`;
|
||||
- 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 check` integration;
|
||||
- imported/module `VTGuarded` lowering to portable `viewGuarded`;
|
||||
- portable guard boundary diagnostics with symbol/application context.
|
||||
Reference in New Issue
Block a user