Tricu 2.0.0

Sorry for squashing all of this but 🤷
This commit is contained in:
2026-05-25 12:43:15 -05:00
parent 2e2db07bd6
commit fdebb6c13d
105 changed files with 10139 additions and 1938 deletions

371
docs/guard-injection.md Normal file
View 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.