From 4f2ede1af4bf8da698b83a49b1fa112b0e2b3de4 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 28 Jul 2026 13:14:15 -0400 Subject: [PATCH 01/15] Create initial vision documents for instruction rewrite --- vision/clock.md | 529 +++++++++++++++++++++++++++++ vision/contents.md | 48 +++ vision/demo.md | 458 ++++++++++++++++++++++++++ vision/design-tradeoffs.md | 139 ++++++++ vision/execution-pipeline.md | 586 +++++++++++++++++++++++++++++++++ vision/implementation-plan.md | 259 +++++++++++++++ vision/instruction-model.md | 554 +++++++++++++++++++++++++++++++ vision/macro-generation.md | 79 +++++ vision/runtime-drivers.md | 372 +++++++++++++++++++++ vision/sst-resolution.md | 155 +++++++++ vision/stack-machine-policy.md | 118 +++++++ vision/traits.md | 56 ++++ vision/vision.md | 240 ++++++++++++++ 13 files changed, 3593 insertions(+) create mode 100644 vision/clock.md create mode 100644 vision/contents.md create mode 100644 vision/demo.md create mode 100644 vision/design-tradeoffs.md create mode 100644 vision/execution-pipeline.md create mode 100644 vision/implementation-plan.md create mode 100644 vision/instruction-model.md create mode 100644 vision/macro-generation.md create mode 100644 vision/runtime-drivers.md create mode 100644 vision/sst-resolution.md create mode 100644 vision/stack-machine-policy.md create mode 100644 vision/traits.md create mode 100644 vision/vision.md diff --git a/vision/clock.md b/vision/clock.md new file mode 100644 index 00000000..53e25147 --- /dev/null +++ b/vision/clock.md @@ -0,0 +1,529 @@ +Clock, send, and recv in execution mode + +Instead of having some scheduler or executor for send/recv, we will have the notion of a clock: +- Per composite clocks, +- Global clocks + +We can think of a "clock" as a timeline; each event that could happen during the execution of the CPU +has some associated unit of time, and we place these on the timeline. Then, we split this timeline up +into ticks, where we define each tick to have a unit of time associated with it. Then, during execution, +while we are working through ticks, each event that has its time within that tick will be executed in the +order of their times. + +Global clocks will be responsible for syncing the clocks of the devices below it, think like a translator; +for example, I have a global clock, and two child CPUs, each with their own clock: +- On CPU 1, ADD takes 3 tick of the global clock +- On CPU 2, ADD takes 1 tick of the global clock + +So when we execute one global clock tick: +- 3 ticks on CPU 1 will pass, and +- 1 tick on CPU 2 will pass. + +We will have some way for instructions to dictate how much time passes. For example: +- send takes one clock tick +- recv takes one clock tick + however long it takes for the data to be received. + +If recv doesn't have the value available, that thread of execution will continue to be parked. Then, it will +be awoken once we have a value, and execution will continue. + +Questions: +- What should the clock look like? +- How do runtimes assign a unit of time to each instruction? Some Tick trait? +- How are clocks synced? +- What should the clock own? + +--- + +## Updated Direction + +The material above records the questions that motivated the clock design. The current direction is +defined together with the architecture mapped in [`contents.md`](./contents.md) and the two-CPU +integration target in [`demo.md`](./demo.md). + +A clock is not a universal vihaco authority and does not replace instruction dispatch, resource +handling, or the driver. Clock implementations are reusable library components built through the +same component and effect model as stacks, arithmetic units, and communication resources. Vihaco +core supplies the boundaries that let those components participate: + +- A composite executes one supplied runtime instruction through `step`. +- Routes may associate execution with timing information. +- Effects can be handled by local components and propagated across nested composites. +- A step returns owned status and driver-facing work. +- Parked execution registers owned continuation state. +- An external driver selects the next eligible work. + +The two-CPU demo chooses one concrete arrangement: + +```text +Runtime +├── TimelineDriver +└── HeterogeneousMachine + ├── GlobalClock + ├── reusable communication component + ├── CpuA + │ └── LocalClock { global_ticks_per_local_cycle: 2 } + └── CpuB + └── LocalClock { global_ticks_per_local_cycle: 3 } +``` + +`GlobalClock` is modeled state inside the top-level composite. `TimelineDriver` remains external so +it can use the clock and CPU state without a field borrowing its containing machine. Another +runtime may place its global clock state inside the driver instead. Clock placement is a runtime +choice, not part of the `Instruction` or `Execute` contracts. + +## Time, Duration, and Local Cycles + +The model distinguishes three quantities: + +- **Global tick** is an absolute position on the definitive machine timeline. +- **Global duration** is a distance between two global ticks. +- **Local cycles** count work in the domain of one child clock. + +They should not be interchangeable integers. Conceptually: + +```rust +pub struct GlobalTick(pub u128); +pub struct GlobalDuration(pub u128); +pub struct LocalCycles(pub u64); +``` + +The exact representation remains a library API decision. Distinct types prevent an absolute time +from being used as a duration and prevent one CPU's local cycles from being mistaken for global +ticks. Arithmetic that advances time or converts cycles must detect overflow rather than silently +wrapping. + +Host execution time has no relationship to modeled time. A slow Rust call can represent zero +modeled duration, while a fast call can schedule work far into the future. + +## Global Clock + +The global clock is the definitive time authority for a particular modeled machine. In the demo it +owns: + +- The current `GlobalTick`. +- An ordered collection of future events. +- A monotonically increasing sequence used to order events at the same tick. +- Any generation or reset state required to reject stale work. + +It does not: + +- Fetch or execute runtime instructions. +- Advance a program counter. +- Borrow a CPU and call its `step` method. +- Interpret arithmetic, communication, or other domain effects. +- Observe every mutation made by every component. + +Those responsibilities belong to the driver, the configured program-counter owner, and typed +effect handlers. + +The global clock can be generic over the event type used by a library or machine: + +```rust +pub struct Scheduled { + pub at: GlobalTick, + pub sequence: u64, + pub event: E, +} +``` + +Events are ordered by `(at, sequence)`. Sequence order makes same-tick behavior deterministic and +prevents device field order or collection iteration order from accidentally changing execution. +Additional phases may later become part of the ordering key if a runtime needs separate evaluation +and visibility stages. + +The first implementation is event-driven. It advances directly to the next scheduled event rather +than visiting every empty global tick: + +```text +remove the earliest event + -> advance GlobalClock.now to its tick + -> return the owned event to the driver + -> driver performs the selected work + -> insert resulting events + -> repeat +``` + +Skipped ticks remain meaningful positions on the timeline; they simply contain no observable work. + +## Local Clocks + +A local clock relates child execution to the global timeline. It is not an independent time +authority. The demo begins with a fixed integer ratio: + +```rust +pub struct LocalClock { + pub cycle: u64, + pub global_ticks_per_local_cycle: u64, +} +``` + +The ratio must be nonzero. A production API may enforce that invariant with construction-time +validation or a nonzero numeric type. + +Conversion follows: + +```text +global duration = + local cycles × global ticks per local cycle +``` + +For the demo: + +```text +CpuA: 1 local cycle × 2 = 2 global ticks +CpuB: 1 local cycle × 3 = 3 global ticks +``` + +Both CPUs may therefore execute the same `add` runtime instruction through the same reusable +arithmetic component and report one local cycle, while becoming eligible at different global +ticks. + +A local clock may be an ordinary component and typed handler. It can accept route completion +information, update its local cycle count, and produce an owned global scheduling request. A debug +component may handle the same completion information for tracing. Both use the same typed handler +model. + +Child clocks do not advance private timelines and later reconcile them with the parent. Their +converted work is scheduled directly on the common global timeline, so global event ordering +defines how child execution interleaves. + +The fixed integer ratio is sufficient for the integration demo. Rational periods, phase offsets, +drift, and clock-domain crossings can be library extensions after this model is proven. + +## Clock and Driver Roles + +A clock and a driver answer different questions: + +| Question | Owner in the demo | +|---|---| +| What is the current definitive tick? | `GlobalClock` | +| Which event is earliest? | `GlobalClock` event ordering | +| Which work does that event represent? | The machine-specific event type | +| Who obtains the corresponding instruction or completion? | `TimelineDriver` through explicit machine operations | +| Who calls `step`? | `TimelineDriver` | +| Who applies returned scheduling requests? | `TimelineDriver`, by inserting them into `GlobalClock` | +| Who advances a CPU program counter? | The CPU's modeled program-counter component | + +The driver loop is: + +```text +read the earliest event from GlobalClock + -> advance global time + -> identify the target CPU or completion + -> obtain an owned runtime instruction or completion + -> call the top-level machine route + -> interpret Complete, Parked, terminal control, and scheduling work + -> return future events to GlobalClock +``` + +The driver must not retain a reference borrowed from a child program while mutably stepping the +whole machine. A CPU-owned program source therefore returns an owned runtime instruction, or the +immutable program is stored outside the mutable composite. + +A clock can itself fill the driver role in another runtime when it is external to the machine and +owns both event selection and the driving loop. The demo keeps the roles separate because its +global clock is explicitly a field of the top-level composite. + +Vihaco must also support drivers with no clock. A sequential interpreter or direct caller can +invoke `step` without modeled time. The existence of `GlobalClock` and `LocalClock` library types +does not make clocks a requirement for a composite. + +## Instruction Timing + +Runtime instructions describe semantic operations. They do not own a clock, event queue, driver, or +universal timing trait. The same `Add` type can have different duration in different routes or +machines. + +Timing information may come from: + +- A route default. +- Optional route metadata. +- Runtime instruction data. +- A component result. +- Resource state. +- Driver configuration. +- An external completion event. + +The initial demo uses route-level local duration: + +```text +add -> 1 local cycle +sub -> 1 local cycle +mul -> 1 local cycle +send -> 1 local cycle +successful recv -> 1 local cycle +``` + +This information does not belong in the reusable arithmetic component. After the route completes, +the selected local clock translates its local duration and emits driver-facing global scheduling +work. + +```text +runtime instruction + -> resolve message + -> execute on selected component + -> handle semantic effects + -> apply route-local timing through LocalClock + -> return status and owned scheduling work +``` + +An instruction that mutates its component and returns `Effects` still receives route +timing. The global clock does not need to observe the mutation or every effect. It only receives the +information required to determine global eligibility. + +A `Tick` trait implemented by every instruction is not required. If repeated timing APIs become +useful after the first implementation, they can describe route or runtime timing without coupling +semantic instruction types to one clock model. + +## Scheduling Requests + +Scheduling work that affects an external driver must cross the `step` boundary as owned data, or be +stored in explicit machine state that the driver drains. Returning owned requests is the clearest +initial model. + +Conceptually, a request identifies when and what becomes eligible: + +```rust +pub struct Schedule { + pub after: GlobalDuration, + pub event: E, +} +``` + +The driver submits the request to `GlobalClock`. The clock converts `after` to an absolute tick +relative to its current `now`, validates the arithmetic, assigns a deterministic sequence, and +inserts the event. An alternative request may already contain an absolute tick when that time comes +from an external source. + +The concrete event sum is machine- or library-specific. Vihaco core does not define `RunCpu`, +`DeliverValue`, or other demo events. It only needs an owned step boundary through which the +configured runtime can communicate scheduling work. + +Scheduling the past is an error. Scheduling at the current tick is allowed when same-tick sequence +ordering defines when the new event becomes visible. + +## Completion, Parking, and Readiness + +Timing does not replace the instruction execution status: + +```rust +pub enum Execution { + Complete, + Parked, +} +``` + +This is the minimal status; the actual step outcome may also contain terminal control and +driver-facing work. + +`Complete` means the instruction and all immediate effect handling have reached a step boundary. If +the program has another instruction, its route normally returns scheduling work based on the local +duration. If the program is exhausted, the CPU leaves the runnable set instead. + +`Parked` means the resource or component has atomically registered an owned continuation and the +driver must not schedule the CPU's next instruction. Parking is a readiness decision, not an +unknown duration added to an otherwise complete instruction. + +When a completion becomes available: + +1. A library handler identifies the parked CPU and continuation. +2. The parent routes the owned completion to that child. +3. The continuation applies its result. +4. The child's local clock accounts for the completion duration. +5. A global event makes the CPU eligible after the converted duration. + +No borrow from resolution, execution, or effect handling survives the parked step. + +## Communication Timing + +`send` and `recv` demonstrate timing, but their resource semantics belong to a reusable +communication library rather than vihaco core. + +For `send`: + +```text +resolve: + consume the local stack value + +execute and handle: + emit an owned library-defined transmission request + account for the route's local duration +``` + +Sender acceptance and value delivery are distinct events. A library may choose: + +- Immediate acceptance with delivery at the current global tick. +- Immediate acceptance with delivery at a future tick. +- Parked acceptance until capacity or a receiver becomes available. + +For `recv`: + +```text +value available: + remove and deliver the value + account for the successful receive duration + complete + +value unavailable: + atomically register an owned continuation + return Parked without scheduling the next instruction +``` + +The atomic check-or-register operation prevents a value from arriving between the availability +check and waiter registration. + +The integration demo initially uses immediate delivery at the sender's current global tick. +Sequence order defines visibility relative to other events at that tick. A later communication +library may introduce transport latency without changing vihaco's clock, instruction, or composite +contracts. + +## Nested Clock and Effect Flow + +The demo uses two levels of composite routing: + +```text +CpuA Add completes with 1 local cycle + -> CpuA LocalClock converts it to 2 global ticks + -> owned scheduling request leaves CpuA + -> HeterogeneousMachine returns it to TimelineDriver + -> TimelineDriver inserts CpuA eligibility into GlobalClock +``` + +`CpuB` follows the same path but converts one local cycle to three global ticks. + +A communication completion follows the inverse direction: + +```text +GlobalClock releases delivery event + -> TimelineDriver routes the owned event through HeterogeneousMachine + -> communication handler identifies the waiting CPU + -> parent forwards the completion into the child + -> child continuation completes recv + -> LocalClock schedules the child's next eligibility globally +``` + +The framework preserves nested route identity and ownership. Clock and communication libraries +define the event contents and resource behavior. + +## Demonstration Trace + +The trace in [`demo.md`](./demo.md) is the acceptance case for the clock model. Its important timing +points are: + +```text +global 0: CpuA add; next eligible at 2 +global 0: CpuB sub; next eligible at 3 +global 2: CpuA send; next eligible at 4 +global 3: CpuB recv completes; next eligible at 6 +global 4: CpuA recv parks +global 6: CpuB mul; next eligible at 9 +global 9: CpuB send satisfies CpuA's receive +global 11: CpuA becomes eligible after one local receive cycle +``` + +This proves: + +- Both CPUs use the same semantic instructions and local duration. +- Local clock configuration produces different global eligibility. +- The global event order is definitive and deterministic. +- A parked receive removes a CPU from normal instruction scheduling. +- Delivery resumes the correct continuation and re-enters the timeline through its local clock. + +## Ownership Boundaries + +The demo assigns ownership as follows: + +| Owner | State and policy | +|---|---| +| Vihaco core | Typed instructions, execution relationships, effects, route generation, step status, and owned driver boundary | +| `GlobalClock` library component | Current global tick, event queue, sequence allocation, and reset generation | +| `LocalClock` library component | Local cycle state and local-to-global conversion policy | +| `TimelineDriver` library item | The loop that selects events, invokes machine work, and applies scheduling requests | +| CPU composite | Local architectural state, selected instruction routes, program, program counter, and parked status | +| Communication library | Values in flight, waiting continuations, acceptance, delivery, and transport timing | +| Runtime instruction | Fully resolved semantic operands | + +Instructions do not own clocks, queues, wakers, or scheduler state. The global clock does not own +component semantics or instruction dispatch. The driver does not mutate private fields directly; +it uses explicit machine operations. + +## Faults, Reset, and Deadlock + +Clock and scheduling faults retain enough context to identify: + +- The current global tick. +- The event sequence and target. +- The CPU and route when applicable. +- The requested duration or absolute tick. +- Whether the failure occurred during conversion, insertion, dispatch, or completion. + +Reset invalidates pending work through a generation or equivalent identity. A completion created +before reset cannot resume a newly reset CPU that happens to reuse the same local identifier. + +The driver detects deadlock when: + +- No runnable CPU remains. +- Every incomplete CPU is parked. +- The global event queue contains no event capable of satisfying a continuation. + +Deadlock is distinct from successful program exhaustion and from waiting on an external event that +the selected driver knows may still arrive. + +## Implementation Sequence + +Clock work should develop alongside the instruction rewrite and demo: + +1. Define distinct global tick, global duration, and local cycle types. +2. Implement a deterministic generic global event queue with checked time arithmetic. +3. Implement fixed-ratio local clock conversion. +4. Add owned scheduling work to the composite step outcome. +5. Drive one clocked CPU through route-local timing. +6. Place two CPU instances under one global clock and verify the two ratios. +7. Add library-defined send delivery. +8. Add parked receive, owned completion, wakeup, and stale-generation protection. +9. Assert the deterministic trace from `demo.md`. + +Each stage should leave a focused runnable test. The concrete `GlobalClock`, `LocalClock`, and +`TimelineDriver` APIs may begin as ordinary library types. Common traits or macro shorthand should +be introduced only after these implementations expose stable repetition. + +## Acceptance Criteria + +The clock model is ready for the integration demo when: + +- Global tick, global duration, and local cycles are distinct types. +- Host execution time never changes modeled time. +- Global time advances monotonically. +- Same-tick events execute in deterministic sequence order. +- Empty spans can be skipped without changing results. +- `CpuA` converts one local cycle to two global ticks. +- `CpuB` converts one local cycle to three global ticks. +- The same arithmetic instruction can have different global duration without knowing either clock. +- A completed route schedules only the next eligible work. +- A parked route schedules no next instruction. +- A communication completion resumes only its registered continuation. +- Resume timing passes through the waiting CPU's local clock. +- Program exhaustion, deadlock, parking, and external waiting are distinguishable. +- Reset prevents stale scheduled work from mutating a new execution generation. +- The global clock remains an ordinary library component rather than a required vihaco core + concept. +- A clockless sequential driver can use the same `step` boundary. + +## Deferred Questions + +The first implementation does not need to decide: + +- Fractional or irrational clock ratios. +- Phase offsets and clock drift. +- Multiple visibility phases within one global tick. +- Stochastic latency. +- Real-time pacing against a wall clock. +- Distributed event queues. +- Dynamic clock-tree reconfiguration. +- General cancellation of in-flight operations. + +These features may extend the library-level clock and driver implementations later. They do not +change the ownership boundaries defined by [`instruction-model.md`](./instruction-model.md), +[`execution-pipeline.md`](./execution-pipeline.md), and +[`runtime-drivers.md`](./runtime-drivers.md), or the integration behavior required by +[`demo.md`](./demo.md). diff --git a/vision/contents.md b/vision/contents.md new file mode 100644 index 00000000..2321c121 --- /dev/null +++ b/vision/contents.md @@ -0,0 +1,48 @@ +# Vihaco Vision Contents + +This directory describes the in-progress vihaco architecture and the reference machine used to +validate it. The current-direction documents below should be read together: each owns a distinct +part of the design, while the demo provides the integration target. + +## Architecture + +Read these documents in order when following the instruction rewrite from its type model through +runtime execution: + +1. [`instruction-model.md`](./instruction-model.md) defines surface and runtime instruction + products, `Instruction` and `Execute`, component responsibilities, composite selection, and + generated machine instruction sums. +2. [`execution-pipeline.md`](./execution-pipeline.md) defines surface resolution and the + route-specific runtime stages of message resolution, component execution, and effect handling. +3. [`runtime-drivers.md`](./runtime-drivers.md) defines step outcomes, program drivers, clock and + driver roles, program-counter ownership, parking, resumption, and fault boundaries. +4. [`stack-machine-policy.md`](./stack-machine-policy.md) applies the ownership model to native + stack operations, arithmetic, locals, heap allocation, printing, calls, and control flow. +5. [`sst-resolution.md`](./sst-resolution.md) defines pattern-based SST parsing, surface-to-runtime + resolution, canonical syntax ownership, and the generated composite parser. +6. [`macro-generation.md`](./macro-generation.md) separates what instruction, component, + composite, and effect-wiring macros generate from what machine authors write. +7. [`design-tradeoffs.md`](./design-tradeoffs.md) records the alternatives considered and the + architecture's observability, debugging, and error-model consequences. +8. [`implementation-plan.md`](./implementation-plan.md) defines test coverage, migration phases, + focused architecture fixtures, deferred questions, and acceptance criteria. + +## Reference Machine and Timing + +- [`demo.md`](./demo.md) is the end-to-end integration target: two reusable CPU composites with + different local clock ratios exchange arithmetic results through a reusable communication + component. +- [`clock.md`](./clock.md) defines the timeline model needed by that demo, including global and + local clocks, deterministic scheduling, driver interaction, parking, communication timing, and + reset behavior. The material above its divider records the earlier questions that motivated the + current design. + +## Earlier Working Notes + +- [`vision.md`](./vision.md) is an early, incomplete architecture sketch. It provides historical + context but includes proposals superseded by the current-direction documents. +- [`traits.md`](./traits.md) is an earlier first-class-traits draft. Its instruction ownership and + capability ideas are exploratory rather than the current implementation plan. + +When these earlier notes conflict with the architecture, reference-machine, or timing documents +above, the current-direction documents take precedence. diff --git a/vision/demo.md b/vision/demo.md new file mode 100644 index 00000000..3d6aad34 --- /dev/null +++ b/vision/demo.md @@ -0,0 +1,458 @@ +# Heterogeneous Two-CPU Demo + +## Purpose + +The vihaco integration reference is a small heterogeneous computer built from reusable parts: + +- One top-level composite owns a definitive global clock. +- The composite contains two CPU composites. +- Each CPU owns a local stack, arithmetic state, a local clock, a program, and a program counter. +- Both CPUs expose `add`, `sub`, `mul`, `send`, and `recv`. +- The CPUs exchange arithmetic results through a shared communication component. +- The two local clocks map their cycles to the global clock at different rates. + +This demo is the concrete forcing case for the architecture mapped in +[`contents.md`](./contents.md). Those documents define the general instruction, component, +composite, effect, and driver boundaries. This document defines a machine that must be expressible +through those boundaries without adding CPU-, channel-, or clock-specific exceptions to vihaco's +core. + +This is the only end-to-end reference runtime. Smaller machines may remain as conformance fixtures +for individual instruction and driver boundaries, but they do not define a competing integration +target. + +The goal is not merely to make the example run. The goal is to show that vihaco supports fast and +correct prototyping of heterogeneous machines by composing ordinary Rust types, selecting a precise +instruction set, and changing configuration rather than rewriting execution logic. + +## Machine Topology + +The demo runtime has an external driver and one top-level machine: + +```text +Runtime +├── TimelineDriver +└── HeterogeneousMachine + ├── GlobalClock + ├── shared communication component + ├── CpuA + │ ├── program and program counter + │ ├── operand stack + │ ├── arithmetic unit + │ ├── communication endpoint + │ └── LocalClock { global_ticks_per_local_cycle: 2 } + └── CpuB + ├── program and program counter + ├── operand stack + ├── arithmetic unit + ├── communication endpoint + └── LocalClock { global_ticks_per_local_cycle: 3 } +``` + +`HeterogeneousMachine` is the single top-level composite. `CpuA` and `CpuB` are two instances of +the same reusable CPU composition unless the implementation reveals a genuine need for different +CPU types. Their instruction semantics are identical. Their clock configuration, programs, local +state, and route identities are distinct. + +The global clock is part of the modeled machine, but it does not call back into its containing +composite. `TimelineDriver` remains external to the machine so it can use the machine's state +without creating a self-borrowing driver field. The driver asks the machine for its next scheduled +work, invokes the appropriate child step, and returns any resulting scheduling work to the global +clock. + +This arrangement intentionally separates: + +- The global clock, which owns the definitive modeled time and event ordering. +- The local clocks, which translate local cycles into global duration. +- The driver, which repeatedly selects eligible work and calls `step`. +- The CPUs, which own their local execution state. + +## Framework, Library, and Demo Boundaries + +The demo uses channel and clock concepts, but those concepts do not become intrinsic vihaco +semantics. Vihaco provides the composition mechanisms; reusable libraries provide particular +machine components. + +| Layer | Responsibilities | +|---|---| +| Vihaco core | Surface/runtime instruction separation, `Resolve`, `Execute`, generated route dispatch, typed effects and handlers, nested composite boundaries, owned step outcomes, parking, and driver integration | +| Reusable component libraries | Stacks, arithmetic units, program storage, program counters, local and global clocks, timeline scheduling, channel endpoints, and a shared channel fabric | +| Demo machine | Selects the five instructions, instantiates two CPUs, assigns clock ratios, wires communication, loads the programs, and chooses initial stack values | + +`ChannelFabric` is therefore an example library component, not a vihaco-level idea. The same is true +of a particular mailbox, interconnect, clock, stack, or arithmetic implementation. Such types may +ship with the vihaco project as useful libraries, but the framework must not contain special cases +for their names or semantics. + +The core requirement is more general: + +- A nested composite can emit an owned effect across its parent boundary. +- The parent can route that effect to any typed handler. +- A handler can later produce an owned completion for the correct child. +- A parked child can resume from that completion. +- Driver-facing scheduling work can leave `step` without retaining borrows. + +A different communication library should be usable without changing vihaco's instruction or +composite machinery. + +## CPU Instruction Set + +Both CPUs select the same five surface and runtime operations: + +```text +add +sub +mul +send +recv +``` + +The CPU composite owns the machine-local routes. Merely containing an arithmetic unit, stack, local +clock, or communication endpoint does not expose every operation offered by those components. + +### Arithmetic + +`add`, `sub`, and `mul` use the same staged path: + +```text +resolve: + consume rhs and lhs from the CPU's local operand stack + +execute: + run the selected reusable arithmetic operation + +handle: + push the result onto the same CPU's local operand stack + account for one local cycle +``` + +The arithmetic component does not know which CPU contains it, which stack supplied the values, or +how long a local cycle lasts globally. The same instruction and component implementations execute +in both CPUs. + +Each arithmetic route initially costs one local cycle. Because the local clocks have different +ratios, the same semantic operation has different global duration: + +```text +CpuA add: 1 local cycle × 2 global ticks = 2 global ticks +CpuB add: 1 local cycle × 3 global ticks = 3 global ticks +``` + +The same conversion applies to `sub` and `mul` in the first version. Later timing models may assign +different local durations per route without changing arithmetic semantics. + +### Send + +`send` consumes a value from the CPU's local stack and targets a communication component supplied +by a reusable library: + +```text +resolve: + consume the value from the local operand stack + use the resolved channel identifier from the runtime instruction + +execute: + validate or prepare the send through the CPU's communication endpoint + +handle: + emit an owned transmission request across the CPU boundary + route it through the parent to the shared communication component + account for one local cycle +``` + +The surface form may contain a symbolic channel name. The machine's `Resolve` implementation turns +that name into the runtime identifier used by the communication library. + +### Receive + +`recv` either obtains a queued value or parks: + +```text +value available: + receive the value + push it onto the local operand stack + account for one local cycle + complete + +value unavailable: + register an owned continuation + emit an owned receive request + return Parked +``` + +When a matching value arrives, the communication library produces an owned completion containing +enough identity to select the CPU and continuation. The parent routes that completion to the parked +CPU, the receive result is placed on its stack, and its local clock determines when the CPU becomes +runnable again. + +No borrow from message resolution, component execution, or effect handling survives the parked +step. + +## Nested Effect Flow + +The demo requires nested composites to communicate with sibling resources without reaching through +their parent's fields. + +For a send from `CpuA` to `CpuB`: + +```text +CpuA Send route + -> transmission effect leaves CpuA + -> HeterogeneousMachine preserves CpuA route identity + -> shared communication handler accepts the effect + -> handler queues or delivers the value + -> completion is routed to CpuB when required +``` + +For a parked receive: + +```text +CpuA Receive route + -> continuation is registered inside CpuA or its endpoint + -> receive request leaves CpuA + -> shared communication handler records the waiter + -> CpuA returns Parked + -> a later send satisfies the waiter + -> owned completion is routed back to CpuA + -> CpuA becomes eligible on the global timeline +``` + +This is ordinary typed effect handling at two composite levels. The framework does not need to know +that the effect represents a channel operation. It only needs to preserve route provenance, +deterministic handler order, ownership across suspension, and the distinction between internal and +driver-facing work. + +The first implementation may use direct generated match arms for this propagation. A generalized +hierarchical effect API is only necessary if the concrete demo reveals repeated code that cannot be +expressed cleanly by the composite declaration. + +## Timing Model + +The global clock is the definitive source of modeled time. Its event queue orders work by: + +```text +(global_tick, deterministic_sequence) +``` + +The sequence value gives stable ordering to events scheduled for the same global tick. Host +execution time never contributes to modeled duration. + +Each CPU route produces or is associated with a duration in local cycles. The selected CPU's local +clock translates that duration into a global scheduling request: + +```text +route completes with local duration + -> local clock applies its configured ratio + -> owned global scheduling request leaves the CPU + -> global clock schedules the CPU's next eligible step +``` + +The timing contract must make the following cases explicit: + +- A completed instruction schedules the CPU's next instruction after its converted duration. +- A parked `recv` does not schedule the next instruction. +- A delivery wakes only the matching continuation. +- Completing a parked receive incurs its configured local duration before the following instruction + becomes eligible. +- Program exhaustion removes the CPU from the runnable set. +- Events at the same global tick use deterministic ordering. + +The communication library owns its transport policy. The initial demo may use immediate delivery at +the sender's current global tick, with sequence ordering defining visibility. A later library may +add fixed, state-dependent, or topology-dependent latency without changing vihaco core. + +## Driver Flow + +`TimelineDriver` repeatedly coordinates the machine: + +```text +read the earliest global event + -> advance GlobalClock.now to that event + -> identify the target CPU or completion + -> obtain an owned runtime instruction or completion + -> call the relevant machine step or handler + -> return scheduling requests to GlobalClock + -> repeat until both programs finish or the machine deadlocks +``` + +The driver may be a reusable library item. The core architecture only requires the one-instruction +`Step` boundary and an owned result that communicates completion, parking, terminal control, and +driver-facing scheduling work. + +Each CPU needs its own program and cursor. For this demo, keeping them in the CPU makes the program +counter modeled child state and demonstrates hardware-owned progression. The driver obtains the +next owned instruction through an explicit top-level operation, allowing any borrow of child +program storage to end before the whole machine is mutably stepped. + +The demo should also distinguish normal completion from deadlock. If both CPUs are parked, no +delivery can satisfy either continuation, and the global event queue is empty, the driver returns a +deadlock result rather than waiting indefinitely. + +## Demonstration Program + +A small deterministic exchange can exercise arithmetic, communication, suspension, and unequal +clock ratios. With the rightmost value treated as the top of each stack: + +```text +CpuA initial stack: [2, 2, 3] +CpuB initial stack: [10, 4] +``` + +The conceptual SST programs are: + +```text +CpuA: + add + send to_b + recv from_b + mul + +CpuB: + sub + recv from_a + mul + send to_a +``` + +The expected value flow is: + +```text +CpuA: 2 + 3 = 5 +CpuA sends 5 to CpuB +CpuB: 10 - 4 = 6 +CpuB receives 5 +CpuB: 6 × 5 = 30 +CpuB sends 30 to CpuA +CpuA receives 30 +CpuA: 2 × 30 = 60 +``` + +With every instruction costing one local cycle and immediate communication delivery, one expected +global trace is: + +```text +global 0: CpuA add; next eligible at 2 +global 0: CpuB sub; next eligible at 3 +global 2: CpuA send 5; CpuA next eligible at 4 +global 3: CpuB recv 5; CpuB next eligible at 6 +global 4: CpuA recv parks +global 6: CpuB mul -> 30; CpuB next eligible at 9 +global 9: CpuB send 30; CpuA receive is satisfied +global 11: CpuA becomes eligible and mul -> 60 +``` + +The exact trace depends on the selected communication timing contract, but the contract and expected +trace must be fixed before the end-to-end test is written. The final observable result for this +configuration is `60` on `CpuA`'s stack, with both programs completed and no parked continuation +left behind. + +## Requirements on the Instruction Rewrite + +The architecture mapped in [`contents.md`](./contents.md) must provide or prove the following +surface area for the demo: + +1. A CPU composite can select only `add`, `sub`, `mul`, `send`, and `recv` from larger reusable + component catalogs. +2. Two instances of the same CPU composite retain distinct machine-local route identities. +3. The top-level composite can address and step either child without exposing all descendant + instructions accidentally. +4. A nested route can propagate an owned effect to its parent, and the parent can route it to a + library-defined handler. +5. A parent can route an owned completion back to the correct child independently of that child's + next program instruction. +6. One effect can reach multiple handlers deterministically, such as a local clock and a diagnostic + trace handler. +7. A step outcome can carry owned driver-facing scheduling work. +8. Parking registers an owned continuation and prevents the driver from scheduling the next + instruction prematurely. +9. Programs and program counters can live in each CPU while the external driver safely obtains an + owned instruction for dispatch. +10. Surface channel names resolve to library-defined runtime identifiers before execution. +11. Generated code preserves typed faults and reports the CPU, route, instruction, global tick, and + failed pipeline stage. + +These requirements constrain the general architecture without making the demo's communication or +clock types part of vihaco core. + +## Reusable Library Deliverables + +The demo should be assembled from reusable items rather than defining all behavior inside the +example: + +- A stack component with invariant-preserving operations. +- Arithmetic runtime instructions and an arithmetic component implementing `add`, `sub`, and + `mul`. +- Surface instruction types and resolution support for those arithmetic operations. +- A local clock component with a configurable local-cycle-to-global-tick ratio. +- A global clock or event-queue component with deterministic ordering. +- A communication endpoint and shared communication component supplied by a library. +- Surface and runtime `send` and `recv` instructions supplied by that communication library. +- Owned send, receive, delivery, and wakeup effects. +- Program and program-counter components suitable for a child CPU. +- A timeline driver suitable for more than this one machine. + +The final crate and module organization can be decided during implementation. The architectural +requirement is that none of these reusable components relies on the private fields or concrete type +of `HeterogeneousMachine`. + +## Implementation Sequence + +The demo should grow alongside the instruction rewrite: + +1. Build one CPU from a stack and reusable arithmetic unit; execute `add`, `sub`, and `mul` through + generated routes. +2. Instantiate the CPU twice in a parent composite and prove that route identity distinguishes the + two instances. +3. Add local clocks, the global clock, and a timeline driver; verify the two clock ratios with only + arithmetic instructions. +4. Add a library-provided communication component and complete non-parking `send`. +5. Add `recv`, owned continuation registration, parking, delivery, and wakeup. +6. Parse both SST programs, resolve channel names, and load the resulting runtime programs into the + two CPUs. +7. Record and assert the deterministic global trace. +8. Document how to replace the clock or communication library without changing vihaco core. + +Each stage should leave a runnable test. Macro ergonomics can improve after the manual relationships +are proven, but the final demo must use the public composition surface intended for downstream +users. + +## Acceptance Criteria + +The demo is complete when: + +- One top-level composite contains the global clock and two CPU composites. +- Both CPUs use the same reusable component and instruction implementations. +- The top-level composite exposes only the intended child operations. +- `CpuA` maps one local cycle to two global ticks. +- `CpuB` maps one local cycle to three global ticks. +- Global time is monotonic and same-tick ordering is deterministic. +- Arithmetic touches only each CPU's local stack. +- Values cross CPUs only through typed effects and library-defined communication handlers. +- `recv` parks when no value is available and resumes without retaining a borrow. +- A parked CPU does not execute its next instruction. +- Both SST programs lower entirely to the selected runtime instruction sums. +- The expected trace is reproducible. +- `CpuA` finishes with `60` on its stack. +- Both programs terminate with no lost value, stale continuation, or pending event. +- Replacing the communication component does not require a change to vihaco core. +- Building `CpuB` from `CpuA` requires configuration and wiring changes rather than copied execution + implementations. + +The last criterion is central to the demonstration. Heterogeneity should arise from composition, +configuration, timing, and program choice while reusable semantic components remain unchanged. + +## Non-Goals + +The first demo does not need: + +- A general network-on-chip model. +- Dynamic CPU discovery. +- Multiple host threads. +- Wall-clock synchronization. +- Nondeterministic or stochastic timing. +- Backpressure beyond what is required to demonstrate a parked receive. +- A complete debugger or visualization frontend. +- Performance representative of physical hardware. + +Those capabilities may be layered onto the same boundaries later. They are not prerequisites for +showing that vihaco can prototype a heterogeneous machine correctly. diff --git a/vision/design-tradeoffs.md b/vision/design-tradeoffs.md new file mode 100644 index 00000000..ac583145 --- /dev/null +++ b/vision/design-tradeoffs.md @@ -0,0 +1,139 @@ +# Design Tradeoffs, Observability, and Errors + +This document records the alternatives behind the selected architecture and the resulting +diagnostic and observability boundaries. + +## Comparison of Alternatives + +The selected component-bound model sits between two simpler designs. Comparing ownership rather +than syntax makes the tradeoff clear. + +### Component-Wide Instruction Enum + +In the component-wide model, state ownership, instruction availability, and dispatch all move +together: + +```text +Component owns: + state + whole instruction enum + whole dispatch + +Composite owns: + collection of components +``` + +Its strengths are: + +- Simple implementation. +- One match performs component dispatch. +- Straightforward single-enum routing. +- Familiar Rust enum ergonomics. + +Its costs appear at composition boundaries: + +- Including a component includes every instruction it supports. +- Unsupported instruction/message combinations may be representable. +- Effects and messages are often coarse enums. +- Component instruction sets are difficult to reuse selectively. +- The machine runtime instruction set is determined accidentally by struct membership. + +### Pure Staged Instructions + +The pure staged model moves all machine state access out of instructions: + +```text +Instruction: + Message -> Result + +Machine: + owns all state resolution and effects +``` + +Its strengths are: + +- Maximum semantic reuse. +- Very easy unit testing. +- Explicit dataflow. +- Strong separation from runtime architecture. +- Excellent observability and simulation potential. + +Its costs appear in stateful machines: + +- Components risk becoming passive storage. +- Local invariant-preserving operations need excessive wiring. +- Simple mutations may require artificial effects. +- The composite carries substantial orchestration code. +- Stateful operations can be awkward or inefficient. + +### Selected Component-Bound Instruction + +The selected component-bound model keeps local state transitions with their owner while making +machine admission and cross-component dataflow explicit: + +```text +Component implements Execute +Composite selects and routes Instruction +``` + +Its strengths are: + +- Selective machine instruction sets. +- Component invariant ownership. +- Typed per-operation messages, effects, and faults. +- Efficient owner-local mutation. +- Explicit cross-component wiring. +- Machine-specific surface parsing and runtime routing. + +Its costs are: + +- Component-bound operations are less portable than pure operations. +- Direct mutations are not automatically visible as effects. +- Duplicate routes require generated route identities. +- Macro and diagnostic complexity increases. +- Cross-component instructions require explicit message/effect staging. +- Tests for stateful instructions need component fixtures. + +This is the default because it places each responsibility at the narrowest stable ownership +boundary. Pure operations remain available through stateless executors, and cross-component +operations deliberately use message and effect staging. + +## Observability and Debugging + +Direct component mutation means not every state change naturally appears in the effect stream. The +architecture does not force artificial command effects solely for observability. The composite +provides step-level hooks, and the driver provides orchestration-level hooks, for: + +- Instruction start and completion. +- Selected route identity. +- Component target. +- Resolved message metadata without exposing sensitive values. +- Emitted effects. +- Execution outcome and faults. +- Modeled start and completion time. + +A component may emit fact events after direct mutation when those events are part of its public +model. Step tracing records route execution; driver tracing records instruction selection, +program-counter changes, parking, wakeups, and modeled time. These hooks remain separate from +semantic effects so enabling diagnostics does not change execution. + +## Error Model + +Failures retain the stage and route in which they occurred. Each `Execute` implementation has a +typed component fault, and the composite converts it into the machine error: + +```rust +MachineFault: From<>::Fault> +``` + +Pattern parsing, module resolution, runtime message resolution, effect handling, and driver +orchestration may also fail. Their diagnostic context identifies: + +- The source instruction and location for parse or module-resolution failures. +- The unresolved label or symbol and the relevant module/function when resolution fails. +- The machine instruction variant. +- The route. +- The target component field. +- The current program position when available. +- The failed stage: parse, module resolve, message resolve, execute, handle, or schedule. + +Conversions preserve the original source chain so machine-level context does not erase the +component or parser failure. diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md new file mode 100644 index 00000000..72f9f47f --- /dev/null +++ b/vision/execution-pipeline.md @@ -0,0 +1,586 @@ +# Instruction Pipeline + +Loading and execution are separate pipelines joined by the runtime program. The first translates +source into resolved instructions; the second executes one of those instructions against live +machine state. + +## Surface Resolution Pipeline + +SST loading follows this path: + +```text +SST text + -> pattern parser + -> ParsedModule + -> Resolve + -> Module + -> runtime program image +``` + +`Resolve` owns every transformation that requires module-wide source +context: + +- Building and consulting label tables. +- Turning `@label` references into `usize` program indices. +- Interning strings. +- Expanding surface sugar into one or more runtime instructions. +- Validating source-level types and declarations. + +At the trait boundary, resolution consumes a parsed surface module and produces a runtime module: + +```rust +pub trait Resolve { + type Module; + + fn resolve_module( + &mut self, + parsed: ParsedModule, + ) -> eyre::Result; +} +``` + +The resolver may delegate individual variants to ordinary helper methods, but the trait remains +module-oriented. That wider view allows it to collect labels before lowering branches, expand one +surface instruction into several runtime instructions, and assign labels to final runtime indices +after expansion. Resolution finishes while constructing the program image; execution never +performs source resolution. + +## Runtime Execution Pipeline + +One-instruction execution starts with a runtime instruction supplied by a driver or direct caller: + +```text +supplied runtime instruction + -> select machine route + -> resolve runtime message + -> execute against the route's component + -> handle immediate internal effects + -> return the step outcome and any driver-facing work +``` + +The composite macro generates one outer `step` dispatcher from the selected runtime routes. Users +do not hand-write this match. A representative expansion is: + +```rust +fn step( + &mut self, + instruction: &MyMachineInstruction, +) -> Result { + match instruction { + MyMachineInstruction::Push(instruction) => { + let message = NoMessage; + let effects = self.operand_stack.execute(instruction, message)?; + self.handle_push_effects(effects) + } + MyMachineInstruction::Add(instruction) => { + let message = self.resolve_add_message(instruction)?; + let effects = self.arithmetic.execute(instruction, message)?; + self.handle_add_effects(effects) + } + MyMachineInstruction::Allocate(instruction) => { + let message = self.resolve_allocate_message(instruction)?; + let effects = self.heap.execute(instruction, message)?; + self.handle_allocate_effects(effects) + } + MyMachineInstruction::ConditionalBranch(instruction) => { + let message = self.resolve_conditional_branch_message(instruction)?; + let effects = self.program.execute(instruction, message)?; + self.handle_conditional_branch_effects(effects) + } + MyMachineInstruction::Send(instruction) => { + let message = self.resolve_send_message(instruction)?; + let effects = self.channels.execute(instruction, message)?; + self.handle_send_effects(effects) + } + } +} +``` + +Every generated match arm performs the same three runtime stages: message resolution, component +execution, and effect handling. The concrete resolver, target field, instruction type, effect +handler, and fault conversions come from that route's composite declaration. `NoMessage` and +`NoEffect` routes reduce to their trivial forms. Directly generated match arms are the initial +representation; further dispatch abstractions are justified only by demonstrated repetition or +compiler constraints. + +`step` does not inherently fetch an instruction, iterate a program, define what happens next, or +advance modeled time. A driver-owned program counter is advanced outside `step`. When a program +counter is itself modeled machine state, route handling may mutate that component during `step`, +but that is an explicit machine configuration rather than universal step behavior. + +### Stage 1: Message Resolution + +Runtime message resolution supplies the owned, execution-time information that is intentionally +absent from the instruction. It is distinct from `Resolve`: module +resolution transforms parsed source into a runtime program, while message resolution reads live +machine state for an instruction that is already fully resolved. + +The composite route owns this stage because only the composite knows: + +- Which stack supplies operands. +- Which program image owns interned strings and function metadata. +- Which register file is active. +- Which frame is current. +- Which privilege or validation policy applies. +- Which clock, resource, or external input belongs to this machine. + +For a stack-machine `Add`: + +```text +resolve: + pop rhs from operand_stack + pop lhs from operand_stack + construct Operands { lhs, rhs } +``` + +For `Print`: + +```text +resolve: + obtain or consume the value selected by the machine's print policy + resolve any interned string data + construct PrintMessage +``` + +For `Send`: + +```text +resolve: + consume the value from the operand stack + retain the channel identifier stored in the instruction + construct SendMessage +``` + +Any route that may park requires an owned message. A synchronous route may borrow data when the +borrow is guaranteed to end before `step` returns. + +#### `NoMessage` + +Instructions with no live input use `NoMessage`, allowing generation to omit a user-written +resolver. A route's documentation still states whether its nontrivial resolution reads, copies, or +consumes machine state. + +### Stage 2: Component Execution + +Component execution applies the resolved operation to its single selected state owner. The call is +synchronous and may: + +- Validate the instruction against component state. +- Mutate the selected component. +- Produce zero, one, or many typed effects. +- Fault. + +The call does not: + +- Borrow arbitrary fields from the composite. +- Retain a borrow into the composite after returning. +- Block on wall-clock I/O. +- Suspend internally. +- Directly schedule future machine steps. +- Choose another component instance by name. + +#### Owner-Local Mutation + +Direct mutation is appropriate when the selected component owns the affected invariant. Typical +examples include: + +- `stack::Push` pushes onto its selected stack. +- `stack::Drop` pops and discards from its selected stack. +- `stack::Dup` duplicates through the stack's invariant-preserving method. +- `heap::Deallocate` deallocates a reference already delivered in its message. +- `counter::Increment` mutates its selected counter. + +Turning these operations into effects that immediately return to the same component adds +indirection without making ownership clearer. + +#### Cross-Component Mutation + +State owned by any other component crosses the composite boundary: + +- Input from another component is resolved into the message. +- Output intended for another component is emitted as an effect. + +This keeps cross-component dataflow visible in the machine definition. + +#### Pure Instructions + +A semantic operation with no mutable state still fits the same relationship: + +```rust +pub struct Add; + +pub struct Operands { + pub lhs: V, + pub rhs: V, +} + +pub struct ArithmeticUnit { + marker: std::marker::PhantomData V>, +} + +impl Execute for ArithmeticUnit +where + V: TryAdd, +{ + type Message = Operands; + type Effect = ValueResult; + type Fault = V::Error; + + fn execute( + &mut self, + _instruction: &Add, + message: Operands, + ) -> Result>, V::Error> { + let value = message.lhs.try_add(&message.rhs)?; + Ok(Effects::one(ValueResult(value))) + } +} +``` + +A zero-sized executor gives the operation an execution target without teaching arithmetic about +stack layout. Pure operations remain a deliberate special case rather than a second execution +pipeline. + +### Stage 3: Effect Handling + +Effect handling gives machine-local meaning to the owned values produced by component execution. +An effect may represent: + +- A result to place in another component. +- A control-flow request. +- A resource request. +- A diagnostic-facing event. +- A scheduling request. +- A request that may park the machine. + +Examples include: + +```rust +pub struct ValueResult(pub V); +pub struct JumpTo(pub JumpTarget); +pub struct Invoke(pub I); +pub struct SendValue { + pub channel: ChannelId, + pub value: V, +} +pub struct Receive { + pub channel: ChannelId, +} +``` + +#### Route-Specific Handling + +The runtime route that produced an effect selects its handling policy. The Rust effect type +describes semantic meaning, but it does not by itself identify a destination within one composite. + +##### Route Identity + +Each entry in a composite's `runtime_instructions` declaration defines one route. Its +composite-local name identifies the complete execution path: + +```text +machine instruction variant + -> runtime instruction type + -> target component field + -> message resolver + -> effect handler + -> route outcome and driver-facing work +``` + +The generated machine instruction variant is the canonical route identity during dispatch. The +`step` match selects the message resolver, target component, effect handler, and route outcome +associated with that variant. Effect handling therefore remains within the route selected by the +outer machine instruction; it is not resolved globally from `Effect`. + +##### Resolution Selects the Runtime Route + +The composite declaration defines the available runtime routes, and the composite macro gives each +one a machine instruction variant. `Resolve` selects among those +variants while lowering surface instructions into the runtime module. + +This separation allows one SST operation to select a machine-specific execution path after its +source operands are resolved. A typed addition illustrates the distinction: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(instruction, head = "arithmetic")] +#[pattern = "'add $ty"] +pub struct SurfaceAdd { + pub ty: SurfaceType, +} +``` + +The same surface product parses both of these forms: + +```text +arithmetic::add integer +arithmetic::add address +``` + +The composite can provide distinct runtime routes for the supported resolved types: + +```rust +runtime_instructions { + IntegerAdd => arithmetic::runtime::Add on integer_arithmetic { + message from operand_stack; + effects to operand_stack; + } + + AddressAdd => arithmetic::runtime::Add on address_arithmetic { + message from address_stack; + effects to address_stack; + } +} +``` + +The composite macro generates the available runtime variants: + +```rust +pub enum MyMachineInstruction { + IntegerAdd(arithmetic::runtime::Add), + AddressAdd(arithmetic::runtime::Add), +} +``` + +The resolver chooses the variant that enters the runtime module. An instruction-specific helper +called by the module-level `Resolve` implementation may take this shape: + +```rust +fn resolve_add( + &mut self, + instruction: SurfaceAdd, +) -> eyre::Result { + match self.resolve_type(instruction.ty)? { + RuntimeType::Integer => Ok(MyMachineInstruction::IntegerAdd( + arithmetic::runtime::Add, + )), + RuntimeType::Address => Ok(MyMachineInstruction::AddressAdd( + arithmetic::runtime::Add, + )), + ty => Err(eyre::eyre!("addition is not supported for {ty}")), + } +} +``` + +The complete transition is: + +```text +surface Add { ty } + -> Resolve validates and resolves ty + -> IntegerAdd(Add) or AddressAdd(Add) + -> step dispatches to the selected arithmetic component + -> route-specific handling returns the result to the selected stack +``` + +The parser does not select a component, and `Execute` does not inspect the composite to choose +one. Resolution makes that architectural decision once, while it has source and type context. The +resulting runtime route then carries the decision through execution and effect handling. + +##### Same Effect Type, Different Machine Semantics + +The same `Add` runtime instruction can therefore appear through two routes: + +```rust +runtime_instructions { + IntegerAdd => arithmetic::runtime::Add on integer_arithmetic { + message from operand_stack; + effects to operand_stack; + } + + AddressAdd => arithmetic::runtime::Add on address_arithmetic { + message from address_stack; + effects to address_stack; + } +} +``` + +Both routes contain the same runtime instruction type and produce `ValueResult`, but they +execute on different component instances and apply their effects to different stacks: + +```text +IntegerAdd -> ValueResult -> operand_stack +AddressAdd -> ValueResult -> address_stack +``` + +A single `Handle> for MyMachine` implementation cannot distinguish these +policies. The effect type intentionally describes the semantic result—an operation produced a +value—without naming a destination in the composite. Adding the destination to `ValueResult` would +couple the arithmetic component to a particular machine layout. Replacing it with a machine-wide +effect enum would reintroduce broad, composite-specific effect types. + +Route-specific handling preserves both abstractions: components emit semantic effects, while the +composite assigns destinations and machine-local behavior. + +##### Generated Route Representation + +`IntegerAdd` and `AddressAdd` originate in the composite's `runtime_instructions` declaration. +Generation turns them into variants of the machine runtime sum: + +```rust +pub enum MyMachineInstruction { + IntegerAdd(arithmetic::runtime::Add), + AddressAdd(arithmetic::runtime::Add), +} +``` + +The outer variant provides route identity inside the generated `step` match. Direct match-arm +generation can inline the corresponding effect handling without introducing another public type. + +Code generation may factor effect handling through `HandleEffects`. In that representation, the +composite macro emits zero-sized internal marker types derived from the route names: + +```rust +#[doc(hidden)] +struct IntegerAddRoute; + +#[doc(hidden)] +struct AddressAddRoute; +``` + +`IntegerAddRoute` and `AddressAddRoute` are generated from machine-local route declarations; they +are not provided by the arithmetic component or the `Add` instruction. They carry no runtime data +and are not part of the component API. Their purpose is to preserve the distinction between +otherwise identical instruction and effect types when handling is expressed through a generic +trait. + +Route identity is required; marker types are not. They are one private representation and disappear +when direct match arms already retain the distinction. + +##### Effect Handling Contract + +Trait-based factoring uses a framework contract such as: + +```rust +trait HandleEffects { + type Effect; + type Error; + + fn handle_effects( + &mut self, + effects: Effects, + ) -> Result; +} +``` + +The composite macro implements this trait for each declaratively wired route. The generated +implementations for `IntegerAdd` and `AddressAdd` are equivalent to: + +```rust +impl HandleEffects for MyMachine { + type Effect = ValueResult; + type Error = MachineFault; + + fn handle_effects( + &mut self, + effects: Effects, + ) -> Result { + for ValueResult(value) in effects { + self.operand_stack.push(value)?; + } + Ok(Execution::Complete) + } +} + +impl HandleEffects for MyMachine { + type Effect = ValueResult; + type Error = MachineFault; + + fn handle_effects( + &mut self, + effects: Effects, + ) -> Result { + for ValueResult(value) in effects { + self.address_stack.push(value)?; + } + Ok(Execution::Complete) + } +} +``` + +The generated `step` arm invokes the implementation associated with its route: + +```rust +MyMachineInstruction::IntegerAdd(instruction) => { + let message = self.resolve_integer_add_message(instruction)?; + let effects = self.integer_arithmetic.execute(instruction, message)?; + >::handle_effects(self, effects) +} +``` + +`resolve_integer_add_message` performs runtime message resolution after the runtime route has +already been selected. It supplies operands from `operand_stack`; it is distinct from the +module-level `Resolve` pass that selected `IntegerAdd`. + +The generated implementation may use a fully qualified trait call, a private method, or an inline +match-arm body. All three preserve the same public model: the current machine instruction variant +selects exactly one effect-handling policy. + +##### Code-Generation Boundary + +Code generation supports the ownership model without becoming part of it: + +| Owner | Responsibility | +|---|---| +| Framework | Defines `Effects`, `Execution`, and, if useful, the generic `HandleEffects` contract | +| User | Defines component state, `Execute` implementations, named routes, the `Resolve` implementation that selects runtime route variants, and custom machine policy | +| Composite macro | Generates the machine instruction variants, exhaustive `step` dispatch, internal route identities when needed, declarative effect forwarding, and fault conversions | +| User, for custom handling | Writes a named handler method when the route cannot be expressed as simple forwarding | +| Composite macro, for custom handling | Generates the route-specific dispatch that calls the user's named method | + +A declaration such as `effects to operand_stack` contains enough information to generate ordinary +forwarding; it does not require the user to write `HandleEffects`. + +A route with custom semantics names a user-defined handler: + +```rust +effects with handle_special_result; +``` + +The macro verifies the handler's effect and outcome types and generates its route-specific call. +A manually implemented composite can implement `HandleEffects` directly, while a generated +composite avoids repetitive route implementations. + +##### Route Provenance + +Route provenance matters whenever two identical effect types receive different machine semantics: + +- The destination component instance. +- Whether a value is pushed, observed, discarded, or transformed. +- Whether handling completes immediately or parks the machine. +- Whether a scheduling request remains internal or crosses the driver boundary. +- Which fault conversion and diagnostic context are attached. +- Which handlers receive the effect. + +Effects therefore do not enter an unlabelled machine-wide queue before route handling. Deferred +work retains either equivalent route provenance or an already-resolved continuation. Once route +handling converts the effect into a resource command, diagnostic event, or driver request, ordinary +typed handlers can continue it without the original route marker. + +#### Effect Ordering + +Effect continuation is deterministic: + +- `Effects::Many` is handled left-to-right. +- Follow-up effects are continued depth-first. +- Route handling produces one final `Execution` outcome. +- A suspending operation occurs only in tail position. + +If one effect parks the machine, the handler must already own or register everything needed to +resume. No borrowed data from resolution or component execution may survive. + +#### Commands and Events + +Commands and events share the same typed transport but carry different meanings: + +- A command effect asking another component or resource to do something. +- A fact/event describing something that already happened during direct mutation. + +Naming and documentation must preserve that distinction. A fact records something that already +happened; it is not a deferred mutation merely because it travels through `Effects`. This matters +for tracing, diagnostic handlers, replay, and future event-sourced runtimes. + +#### No Effects + +Owner-local mutation may complete with an empty `Effects`. The route still returns a step +outcome, and the driver can still account for time or select more work. Neither effect production +nor a clock is required by `step`. diff --git a/vision/implementation-plan.md b/vision/implementation-plan.md new file mode 100644 index 00000000..b0fd2ef1 --- /dev/null +++ b/vision/implementation-plan.md @@ -0,0 +1,259 @@ +# Instruction Rewrite Verification and Migration + +This document turns the architecture into test coverage, migration phases, implementation +questions, and acceptance criteria. + +## Testing Strategy + +Tests follow the same boundaries as the architecture. Narrow tests establish each product and trait +relationship; route and end-to-end tests prove that generation composes them without widening the +machine's public instruction set. + +### Surface Instruction Tests + +Each surface instruction is tested for: + +- Pattern parse round trip for the canonical dialect-qualified form. +- Generated default pattern equivalence where a default is allowed. +- Tuple-index and named-field binding order. +- Nested value/type field parsers. +- Preservation of unresolved names, labels, and symbolic operands. +- Invalid source syntax rejection. + +### Resolution Tests + +Each `Resolve` implementation is tested for: + +- Successful lowering to the expected runtime instruction or instruction sequence. +- Label and symbol replacement with the correct program-image indices. +- Errors for missing, duplicate, or invalid targets. +- Sugar expansion order. +- Machine-specific validation that requires module context. + +The `ConditionalBranch` reference case anchors the boundary: `@foo` survives parsing as a source +label and becomes a `usize` program index only during module resolution. + +### Runtime Instruction Tests + +Each runtime instruction is tested for: + +- Construction with fully resolved values. +- Validation of resolved indices and identifiers where applicable. +- Confirmation that no unresolved source-level names remain. + +### Component Execution Tests + +Each `Execute` implementation is tested for: + +- Successful local state transition. +- Fault behavior. +- Message/instruction pairing. +- Emitted effects. +- Documented partial mutation behavior. + +### Composite Route Tests + +Each composite route test establishes that: + +- The surface instruction is present in the machine surface sum. +- The resolved runtime instruction is present in the machine runtime sum. +- The expected field is selected. +- Message data comes from the correct components. +- Effects reach the correct handlers. +- Duplicate instruction types routed to different fields remain distinct. +- Optional route metadata reaches the configured driver. +- Only explicitly selected surface instruction patterns are accepted. +- Prefix-related mnemonics select the correct route regardless of route declaration order. + +### Compile-Fail Tests + +Compile-fail coverage proves that invalid relationships cannot be generated. It rejects: + +- A selected instruction unsupported by its target component. +- Duplicate public variant names. +- Missing message wiring. +- Missing effect handlers. +- Incompatible message or effect types. +- A suspending effect without a continuation-capable handler. +- A selected surface instruction that does not implement `Parse`. +- A machine surface sum with no applicable `Resolve` + implementation. +- Attempting to route a surface instruction directly to component execution. +- Invalid pattern field mappings and unsupported pattern literals. + +### End-to-End Tests + +End-to-end machines cover: + +- A stack-local instruction. +- A pure arithmetic instruction using stack resolution and handling. +- A heap operation spanning stack and heap. +- A control-flow effect. +- An effect handled by both a stateful component and a diagnostic component. +- A parked receive and resumed continuation. +- A sequential driver with a driver-owned cursor. +- A timeline driver coordinating a global clock with child clocks. +- A machine-owned program counter changed by a modeled hardware component. +- A nested composite exposing only selected operations. +- Pattern parsing into a surface instruction, module resolution into a runtime instruction, and + runtime message resolution before execution. + +## Migration Plan + +Migration proceeds from the semantic relationships outward. Manual instruction and execution types +establish the model first; generation follows only after the required relationships are concrete. + +### Phase 1: Establish the Two Instruction Levels + +1. Establish distinct surface and runtime instruction types. +2. Decide the final names for surface instructions, runtime instructions, and their generated + machine sums. +3. Use the pattern parser generator for all instruction, value, and type surface syntax. +4. Make `Resolve` the explicit lowering boundary. +5. Add a reference branch instruction whose surface form contains labels and whose runtime form + contains resolved `usize` program indices. +6. Test that the generated machine surface sum resolves into a module containing only variants from + the generated runtime sum. + +### Phase 2: Introduce Per-Instruction Component Execution + +1. Add the `Execute` relationship. +2. Add `NoMessage`, `NoEffect`, and typed fault conventions. +3. Implement several manual examples before designing ergonomic macros. +4. Start with stack-native `Push`, `Drop`, and `Dup`. +5. Add one pure operation such as `Add`. +6. Add one cross-component operation such as `Allocate`. + +### Phase 3: Generate Explicit Composite Routes + +1. Extend or replace `#[composite]` with explicit instruction selection. +2. Generate a machine surface-instruction sum and a machine runtime-instruction sum from the + selected routes. +3. Generate the pattern-based machine parser from only the selected surface instructions. +4. Generate the outer runtime dispatch match. +5. Support the same runtime instruction type routed to multiple fields. +6. Require the resolver's output module to use the selected machine runtime sum. + +### Phase 4: Add Message Resolution and Effect Wiring + +1. Generate `NoMessage` and `NoEffect` defaults only when no explicit policy is present. +2. Add route-local runtime message resolver methods. +3. Add route-local effect handling. +4. Support deterministic delivery of one effect to multiple typed handlers. +5. Define deterministic ordering for multiple and follow-up effects. + +### Phase 5: Add Drivers, Timing, and Suspension + +1. Establish the one-instruction `Step` boundary and its owned outcome. +2. Add a sequential driver with an explicitly owned program cursor. +3. Add `Complete` and `Parked` driver semantics. +4. Add owned continuation registration for `Receive`. +5. Reject borrowed continuation state. +6. Add a timeline driver that owns global time and consumes driver-facing scheduling requests. +7. Demonstrate a child clock as an ordinary component and handler. +8. Demonstrate a machine-owned program counter controlled by a modeled hardware component. +9. Test reset generations and stale completions. + +### Phase 6: Migrate Existing Components + +1. Split each component-wide instruction enum into individual surface and runtime instruction + structs. +2. Group source files by semantic family: stack, arithmetic, heap, control flow, I/O, and runtime + metadata. +3. Give every surface instruction its canonical `#[syntax_class(instruction, head = ...)]` and + `#[pattern = ...]` declarations. +4. Move special field grammars into local value/type syntax types where practical. +5. Represent sugar, interning inputs, labels, and other unresolved operands explicitly in surface + instruction types. +6. Implement `Resolve` to lower those forms into executable runtime instructions. +7. Move component-local mutations to `Execute` implementations. +8. Move cross-component reads into runtime message resolution. +9. Move cross-component writes and scheduling into effect handling. + +### Phase 7: Remove Automatic Instruction Inheritance + +1. Stop generating one machine variant per component instruction enum. +2. Require explicit route selection for new composites. +3. Deprecate the component-wide `GeneratedComponent::Instruction` association. +4. Remove adapters after downstream code and documentation have migrated. + +## Additional Architecture Coverage + +[`demo.md`](./demo.md) is the only end-to-end reference runtime. It exercises nested composites, +heterogeneous clocks, arithmetic reuse, cross-device communication, suspension, and timeline +driving as one coherent machine. + +The demo does not need to contain every operation used to validate the instruction architecture. +The remaining boundaries are better established through focused component tests, route tests, and +small conformance fixtures: + +| Coverage case | Architectural boundary | Test scope | +|---|---|---| +| `Push` and `Drop` | Owner-local stack mutation requires no self-directed effect, while composite selection still controls instruction availability | Component and route tests | +| `Load` | A component-local load may mutate one combined stack/frame owner, while split storage uses message resolution and effect handling | Alternative route fixtures | +| `Allocate` | Values move from a stack to a heap and a reference returns through typed cross-component stages | Focused composite fixture | +| `ConditionalBranch` | SST labels survive parsing, resolve to runtime program indices, and update either a driver-owned or machine-owned program counter | Resolver and control-flow fixture | +| `Call` | Program metadata, call-stack mutation, frame construction, return placement, and program-counter policy remain distinct responsibilities | Focused control-flow fixture | +| `Print` | Value acquisition remains separate from output delivery, and one effect may reach output and diagnostic handlers | Focused handler fixture | +| Simple sequential execution | `step` remains usable without a clock, and a driver-owned cursor can advance a resolved program | Small end-to-end fixture | + +These cases do not need to be assembled into a second general-purpose machine. Their purpose is to +prove individual boundaries that the two-CPU demo does not exercise directly. The sequential +fixture is an implementation milestone and a fast test harness, not another reference runtime. + +## Questions to Revisit After the First Implementation + +Several API choices depend on evidence from the first implementation: + +1. The final names for surface instructions, runtime instructions, and their generated sums. +2. How one-to-many lowering is represented while `Resolve` builds the runtime module. +3. Whether execution should eventually return something other than `Effects`. +4. Whether pure operations use zero-sized executor components or a dedicated adapter. +5. How fact events emitted after direct mutation are distinguished from command effects. +6. Whether canonical dialect heads are always fixed by surface instruction types or may be wrapped + by an explicit machine-local surface instruction type. +7. Which validation belongs in pattern parsing and which belongs in `Resolve`. + +Borrow-specific APIs and macro shorthand follow the same rule: they are introduced in response to +concrete compiler friction or repeated boilerplate, not as prerequisites for the architecture. + +These questions do not change the central ownership decision: + +> Components own their state and per-instruction execution; composites own instruction admission, +> route dispatch, cross-component dataflow, and effect routing; drivers own program iteration, +> readiness, scheduling, and modeled time; either a driver or one modeled component owns +> program-counter transitions. + +## Acceptance Criteria + +The rewrite has established the architecture when all of the following are true: + +- Adding a component field does not automatically add runtime instructions. +- A composite can select two runtime instructions from a component that executes ten. +- A composite can admit a surface form without assuming a one-to-one runtime counterpart. +- The same instruction can be routed to two component instances without trait conflicts. +- An unsupported surface instruction is rejected by the generated pattern parser. +- An individual surface instruction struct can derive its canonical parser with + `#[syntax_class(instruction, head = ...)]` and `#[pattern = ...]`. +- The generated machine parser admits only selected surface instruction patterns. +- Pattern parsing, `Resolve`, runtime message resolution, execution, and effect handling remain + distinct stages. +- A surface `ConditionalBranch` can contain `@foo`, while its runtime counterpart contains only a + resolved `usize` program index. +- Runtime instructions contain no unresolved source labels, names, or sugar. +- Only runtime instructions are dispatched to components. +- Native stack mutation requires no artificial self-directed effect. +- Arithmetic can be reused without knowing about stack layout. +- Heap allocation can move values across stack and heap through typed stages. +- Effects are routed deterministically and with route provenance. +- A receive instruction can park and resume without retaining borrows. +- The same composite can be run by a simple sequential driver or a timeline driver. +- Calling `step` directly does not require a program, program counter, or clock. +- Program storage and cursor state can have different owners. +- A machine-owned program counter can be advanced by modeled hardware without competing with + driver-owned advancement. +- Driver-facing scheduling requests cross the step boundary as owned state. +- Nested composites expose only their selected public instruction set. +- Compile errors identify the route and missing component/message/effect relationship. +- Existing diagnostic-handler and loader concepts can integrate without becoming the semantic + owner of instruction execution. diff --git a/vision/instruction-model.md b/vision/instruction-model.md new file mode 100644 index 00000000..28d3eea0 --- /dev/null +++ b/vision/instruction-model.md @@ -0,0 +1,554 @@ +# Hybrid Component-Bound Instruction Architecture + +## Status and Direction + +Vihaco needs instructions to remain reusable without reducing components to passive storage. A +pure instruction model (i.e. everything is an effect) makes dataflow explicit, but forces even +owner-local state changes through the composite. A component-owned instruction-set model +preserves local invariants, but exposes every instruction carried by every selected component +(i.e. composites must support *every* instruction from each of its composites). + +This architecture takes the useful boundary from each model. Instructions remain individually +selectable types, while components retain responsibility for executing the operations that mutate +their state. + +The heterogeneous two-CPU machine in [`demo.md`](./demo.md) is the integration reference for these +boundaries. The instruction rewrite and demo should develop together: the general +architecture must support the demo without introducing CPU-, clock-, or communication-specific +behavior into vihaco core. + +The model has the following properties: + +- Instructions remain individual Rust structs so that a machine can select them independently. +- Surface instructions describe SST syntax and are parsed exclusively by the pattern parser. +- Runtime instructions contain fully resolved operands and are the only instructions executed by + components. +- The machine's `Resolve` implementation lowers surface instructions + into runtime instructions before execution. +- Components remain the owners of state and the invariant-preserving operations over that state. +- A component implements execution for each instruction it supports. +- A composite explicitly selects the instructions that are part of its public instruction set. +- The composite owns machine-level instruction dispatch, message resolution, and effect routing. +- An external driver owns program iteration and any scheduling or modeled time policy needed by + that execution mode. Program-counter transitions have one configured owner: either the driver or + a modeled machine component. +- An instruction may directly mutate the one component selected as its execution target. +- Cross-component inputs and outputs are represented through message resolution and effects. + +The resulting ownership model is: + +| Decision | Owner | +|---|---| +| What syntax is accepted from SST? | Surface instruction types and their patterns | +| How are labels, symbols, and sugar lowered? | The implementer of `Resolve` | +| What fully resolved data is stored for execution? | Runtime instruction types | +| Which component knows how to execute it? | The selected component's `Execute` implementation | +| Is the instruction available in this machine? | The composite | +| Which component instance receives it? | A composite route | +| Where does non-inline input come from? | Composite message resolution | +| Where do results and effects go? | Composite effect handling | +| Who advances the program counter? | Either the driver or one modeled component, never both | +| How much modeled time passes? | The selected driver, using route, component, or effect data | +| Can execution park or resume? | The driver together with the resource that owns the continuation | + +Components may publish a catalog of operations they can execute, but that catalog is not the +machine's instruction set. The composite selects individual instructions and gives each selection a +machine-local route. + +## Goals + +The architecture is intended to preserve the following properties: + +1. A composite exposes only surface and runtime instructions it explicitly selects. +2. Unsupported surface instructions cannot be parsed, and unsupported runtime instructions cannot + be dispatched by that composite. +3. Each instruction has statically paired message, effect, and fault types for a particular + component implementation. +4. A component can preserve its own invariants without converting every local mutation into an + effect. +5. Cross-component data movement remains explicit in the composite. +6. A reusable semantic instruction can be executed in more than one machine architecture. +7. The same instruction can be routed to multiple instances of the same component type. +8. Synchronous execution remains easy to inline and statically dispatch. +9. Suspension remains limited to instruction boundaries. +10. Nested composites can expose a selected instruction set without leaking all instructions from + their children. +11. The generated surface remains ordinary Rust that could be written manually. +12. Surface instruction products use the checked pattern parser generator. +13. Runtime instruction products never contain unresolved labels or other source-only data. +14. A composite parser is constructed from only the selected surface instructions. +15. `Resolve` is the explicit, type-checked bridge from parsed surface + modules to runtime modules. + +## Non-Goals + +The first implementation deliberately leaves the following capabilities outside the core model: + +- Roll back state automatically when an instruction faults. +- Permit a borrowed execution context to survive a parked instruction. +- Infer modeled time from how long host execution takes. +- Dynamically discover instructions at runtime. +- Require all component state transitions to be observable effects. +- Make every instruction portable across every machine architecture. +- Decide advanced borrowing or projection ergonomics before the first implementation demonstrates + that they are needed. + +## Instruction Set Shape: Products Selected Into a Sum + +Surface syntax and runtime execution require different representations. They are separate product +types because source-level names are useful during parsing, while execution requires operands that +have already been resolved. + +```rust +use vihaco_parser::Parse; + +// Surface syntax: appears in SST and may contain source-level names. +#[derive(Parse)] +#[syntax_class(instruction, head = "control")] +#[pattern = "'conditional_branch `@` $when_true `,` `@` $when_false"] +pub struct SurfaceConditionalBranch { + pub when_true: String, + pub when_false: String, +} + +// Runtime instruction: stored in the program image and executed. +pub struct ConditionalBranch { + pub when_true: usize, + pub when_false: usize, +} +``` + +The pattern parser constructs `SurfaceConditionalBranch` from source such as: + +```text +control::conditional_branch @then, @otherwise +``` + +The resolver owns the label table and lowers those names to runtime program indices. The +instruction-specific part can remain a normal helper: + +```rust +impl MyResolver { + fn resolve_conditional_branch( + &mut self, + instruction: SurfaceConditionalBranch, + ) -> eyre::Result { + Ok(ConditionalBranch { + when_true: self.label_index(&instruction.when_true)?, + when_false: self.label_index(&instruction.when_false)?, + }) + } +} +``` + +A composite selects products into two related sums: + +```rust +pub enum MyMachineSurfaceInstruction { + Push(surface::Push), + Add(surface::Add), + ConditionalBranch(surface::ConditionalBranch), +} + +pub enum MyMachineInstruction { + Push(runtime::Push), + Add(runtime::Add), + ConditionalBranch(runtime::ConditionalBranch), +} +``` + +The surface sum is parsed from SST. An implementation of +`Resolve` produces a +`Module` containing runtime instructions for the program image. The +runtime sum is dispatched during execution. + +The mapping is not necessarily one-to-one. One surface instruction may expand into several runtime +instructions, including cases where one source operation selects different execution paths for +different resolved types. A runtime instruction may also be introduced during lowering without a +direct surface form. The invariant is that only runtime instructions reach components. + +A component package may publish surface and runtime instruction catalogs. Those catalogs are not +automatically inherited by a machine; the composite explicitly selects both its accepted surface +syntax and its executable runtime instruction set. The resolver defines the mapping between the two +selected sets rather than requiring every surface operation to name exactly one runtime operation. + +## Core Trait Shape + +A runtime instruction identifies a fully resolved operation. The fact that a particular component +can execute that operation is a separate relationship. Keeping those facts separate allows one +instruction type to participate in several component implementations without giving the +instruction global knowledge of machine state. + +The `Instruction` trait is a marker for runtime operations. Execution behavior belongs to +`Execute`: + +```rust +pub trait Instruction { + // Surface parsing is a separate type-level concern. +} + +pub trait Execute +where + I: Instruction, +{ + type Message: Message; + type Effect: Effect; + type Fault; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} +``` + +The essential relationship is: + +```text +Component implements Execute +``` + +It replaces a component-wide associated instruction set: + +```text +Component has one associated InstructionSet enum +``` + +Each supported operation receives its own implementation: + +```rust +pub struct Stack { + values: Vec, +} + +pub struct Push { + pub value: V, +} + +pub struct Drop; + +impl Execute> for Stack { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = StackFault; + + fn execute( + &mut self, + instruction: &Push, + _message: NoMessage, + ) -> Result, StackFault> { + self.push(instruction.value.clone())?; + Ok(Effects::none()) + } +} + +impl Execute for Stack { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = StackFault; + + fn execute( + &mut self, + _instruction: &Drop, + _message: NoMessage, + ) -> Result, StackFault> { + self.pop()?; + Ok(Effects::none()) + } +} +``` + +The syntax remains illustrative. `NoEffect`, for example, may be uninhabited because +`Effects` never needs to construct a value. + +### Why `Execute for Component` + +Placing execution on `Execute for Component` keeps state ownership visible in the type system: + +- The component is visibly responsible for maintaining its invariants. +- An instruction does not need one globally fixed `Component` associated type. +- The same instruction can have implementations for more than one component type. +- Associated message, effect, and fault types may depend on both the instruction and component. +- A stateless or pure instruction can use a zero-sized executor component. +- Tests can replace a component with a small alternative implementation when useful. + +An `Instruction` trait with `execute(&self, &mut C, ...)` can express the same call +mechanically, but it places component behavior on the instruction side and encourages broad +generic state bounds. The public model instead states the ownership relationship directly: +components execute operations. + +### Instruction Identity and Route Identity + +Instruction identity describes an operation, but not its complete path through a machine. A +composite may route the same instruction type to two fields: + +```rust +pub enum MachineInstruction { + PushOperand(stack::Push), + PushCall(stack::Push), +} +``` + +Both variants contain the same instruction type and may target the same `Stack` component +type, but they target different instances and may have different message and effect policies. + +The outer variant is therefore part of the route identity. The composite uses it to determine: + +- Target field selection. +- Message resolution. +- Effect handling. +- Optional metadata made available to a driver. +- Tracing and diagnostics. +- Machine-local instruction metadata. + +The generated dispatch must preserve that distinction. Whether it does so with direct match arms or +private marker types remains an internal choice; route identity itself is part of the architecture. + +## Shape of Surface and Runtime Instructions + +A surface instruction preserves the information written in SST: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(instruction, head = "control")] +#[pattern = "'branch `@` $target"] +pub struct SurfaceBranch { + pub target: String, +} + +#[derive(vihaco_parser::Parse)] +#[syntax_class(instruction, head = "control")] +#[pattern = "'call $arity `,` `@` $target"] +pub struct SurfaceCall { + pub arity: u32, + pub target: String, +} +``` + +A runtime instruction contains the resolved information required by execution: + +```rust +pub struct Branch { + pub target: usize, +} + +pub struct Call { + pub arity: u32, + pub target: usize, +} +``` + +Surface instruction types therefore: + +- Derive `vihaco_parser::Parse`. +- Own their pattern and dialect head. +- May contain labels, symbolic names, literals, and other source-level values. +- Are inputs to `Resolve`. +- Are never executed by components. +- Are not stored in the runtime program image. + +Runtime instruction types: + +- Contain no unresolved source symbols. +- Implement the runtime instruction marker. +- Are stored in the program image. +- Are the types accepted by `Execute`. +- Need not implement `Parse`. + +Neither representation carries runtime ownership or orchestration state: + +- A reference to its component. +- A reference to the composite. +- A clock or scheduler. +- An event queue. +- A waker. +- A borrowed execution context. +- Runtime scheduler state. + +Runtime instructions may contain resolved semantic configuration such as: + +- An arithmetic type. +- A local index. +- A resolved program index. +- A channel identifier. +- An immediate value. +- An operation mode. + +Information that depends on live machine state belongs in the runtime message rather than either +instruction representation. + +## Shape of a Component + +A component owns one coherent domain of state and the operations that preserve that domain's +invariants. Its responsibilities are to: + +- Store one coherent domain of state. +- Expose invariant-preserving domain methods. +- Implement `Execute` for the individual runtime instructions it supports. +- Implement reset, loading, observation, or resource interfaces when those responsibilities + actually belong to the component. +- Avoid exposing its internal fields merely so generated code can mutate them. + +It does not: + +- Define one enum containing all supported instructions. +- Expose one dispatch method matching every instruction. +- Contribute all its instructions to any composite that contains it. +- Know which machine-local route name a composite assigns to an instruction. +- Know which other components receive its effects. +- Know the runtime's clock or scheduling policy. + +The component's public catalog describes which runtime operations have implementations for its +type. The composite separately decides which surface operations are accepted, how they resolve, +which runtime operations exist in the machine, and which component instance receives each one. + +### Components That Are Also Resources + +Stacks, heaps, channels, and clocks may serve both as instruction targets and as resources used by +message resolution or effect handling. Their ordinary Rust methods remain the +invariant-preserving boundary in both roles: + +```rust +impl Stack { + pub fn push(&mut self, value: V) -> Result<(), StackFault> { + // Preserve capacity, frame, and ownership invariants here. + } + + pub fn pop(&mut self) -> Result { + // Preserve underflow and frame-boundary invariants here. + } +} +``` + +Calling these methods from composite wiring does not expose `Push` or `Pop` as program +instructions. Program visibility changes only when the composite selects a route into its machine +instruction sum. + +## Shape of a Composite + +A composite is the architectural junction between reusable component behavior and one concrete +machine. It is: + +- The product of its component fields. +- The authority that selects its instruction sum. +- The owner of machine-level routing. +- The boundary for cross-component data movement. +- The place where one-instruction route policy becomes concrete. + +Program iteration, program-counter advancement, modeled time, and selection of the next runnable +machine are separate concerns. The driver owns iteration, readiness, and modeled time. Cursor +advancement belongs either to the driver or to an explicitly modeled component; it does not become +an implicit composite responsibility merely because the composite owns instruction dispatch. + +For example: + +```rust +pub struct MyMachine { + operand_stack: Stack, + call_stack: Stack, + arithmetic: ArithmeticUnit, + heap: Heap, + channels: Channels, + program: Executor, + clock: ChildClock, +} +``` + +Merely placing these fields in the struct does not add surface or runtime instructions. The +composite declares its accepted surface instructions and executable runtime routes separately: + +```rust +machine! { + composite MyMachine { + operand_stack: Stack, + call_stack: Stack, + arithmetic: ArithmeticUnit, + heap: Heap, + channels: Channels, + program: Executor, + clock: ChildClock, + } + + surface_instructions { + Push => stack::surface::Push; + Add => arithmetic::surface::Add; + Allocate => heap::surface::Allocate; + ConditionalBranch => control_flow::surface::ConditionalBranch; + Send => channel::surface::Send; + } + + runtime_instructions { + Push => stack::runtime::Push on operand_stack; + + Add => arithmetic::runtime::Add on arithmetic { + message from operand_stack; + effects to operand_stack; + } + + Allocate => heap::runtime::Allocate on heap { + message from operand_stack; + effects to operand_stack; + } + + ConditionalBranch => control_flow::runtime::ConditionalBranch on program { + effects to program; + } + + Send => channel::runtime::Send on channels { + message from operand_stack; + effects to clock; + } + } +} +``` + +The syntax is illustrative; the architecture requires the following properties: + +- Each surface instruction is explicitly admitted to SST parsing. +- The machine's `Resolve` implementation may lower each surface instruction to one or more of the + selected runtime instructions. +- Each runtime instruction has a stable machine-local name. +- Each runtime instruction selects exactly one primary execution target. +- Message and effect wiring is route-specific. + +### Generated Surface and Runtime Sums + +From those declarations, the composite produces one sum for each instruction level: + +```rust +pub enum MyMachineSurfaceInstruction { + Push(stack::surface::Push), + Add(arithmetic::surface::Add), + Allocate(heap::surface::Allocate), + ConditionalBranch(control_flow::surface::ConditionalBranch), + Send(channel::surface::Send), +} + +pub enum MyMachineInstruction { + Push(stack::runtime::Push), + Add(arithmetic::runtime::Add), + Allocate(heap::runtime::Allocate), + ConditionalBranch(control_flow::runtime::ConditionalBranch), + Send(channel::runtime::Send), +} +``` + +The pattern parser generator builds the parser for `MyMachineSurfaceInstruction` from the selected +surface patterns. The resolver builds a module containing `MyMachineInstruction` values. + +The surface sum defines what the parser accepts. The runtime sum defines what `step` can dispatch. +Neither sum inherits unselected instructions from component catalogs. + +### Nested Composites + +A nested composite behaves like a component at its parent's boundary while retaining its own +instruction-selection boundary. It exports only the surface and runtime operations that it has +chosen to make public. The parent may: + +- Route a nested instruction set as a whole when that is intentional. +- Select explicit public operations exported by the child. +- Treat the child as a resource or effect handler without exposing its runtime instructions. + +Containment never implies recursive instruction inheritance. + diff --git a/vision/macro-generation.md b/vision/macro-generation.md new file mode 100644 index 00000000..758ebf40 --- /dev/null +++ b/vision/macro-generation.md @@ -0,0 +1,79 @@ +# Macro Responsibilities + +Macros materialize the relationships declared by instruction, component, and composite authors. +They validate and generate repetitive dispatch, but do not decide which operations a machine +contains or where data flows. + +## Instruction Declaration + +Surface and runtime declarations carry different responsibilities: + +| Declaration | Responsibilities | +|---|---| +| Surface instruction | Pattern, dialect head, parsed source fields | +| Runtime instruction | Fully resolved fields and execution identity | + +For example: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(instruction, head = "control")] +#[pattern = "'branch `@` $target"] +pub struct SurfaceBranch { + pub target: String, +} + +#[derive(Instruction)] +pub struct Branch { + pub target: usize, +} +``` + +The runtime instruction derive does not introduce source syntax. Parsing remains exclusively a +surface-instruction concern. + +## Component Declaration + +The component macro: + +- Associates a component with the runtime instructions it can execute. +- Validates per-runtime-instruction execution signatures. +- Does not require one component-wide instruction enum. +- Does not generate a component-wide dispatch match. + +Ordinary `impl Execute for C` remains the underlying API. The initial implementation establishes +that relationship directly before adding shorthand for repeated declarations. + +## Composite Declaration + +The composite/machine macro: + +- Collects explicitly selected surface instructions and runtime instruction routes as separate + sets. +- Rejects duplicate public variant names. +- Verifies that each target field implements `Execute`. +- Generates the surface instruction sum and its pattern parser. +- Requires every selected surface instruction to implement `vihaco_parser_core::Parse<'src>`. +- Generates the runtime instruction sum. +- Requires a `Resolve` implementation whose output module uses + the machine runtime instruction sum. +- Generates the outer execution match. +- Generates or calls route-specific message resolvers. +- Generates or calls route-specific effect handlers. +- Applies machine fault conversions. +- Attaches optional route metadata that a configured driver may consume. +- Preserves component and source-symbol metadata needed by loaders. + +## Effect Wiring + +Effect wiring supports: + +- One effect sent to one handler. +- One effect sent through a deterministic chain. +- One effect broadcast to multiple handlers. +- A route-local handler method. +- A default handler when the effect is `NoEffect`. + +Wiring remains type-checked. Macro input may contain strings for field names or source aliases, but +generated execution never performs string-based runtime routing. + diff --git a/vision/runtime-drivers.md b/vision/runtime-drivers.md new file mode 100644 index 00000000..45725678 --- /dev/null +++ b/vision/runtime-drivers.md @@ -0,0 +1,372 @@ +# Execution Outcomes and Runtime Drivers + +This document defines the boundary between one-instruction execution and the policies that select, +schedule, park, and resume work. + +## Execution Outcomes, Suspension, and Time + +After immediate effect handling, the route reaches a one-instruction execution state: + +```rust +pub enum Execution { + Complete, + Parked, +} +``` + +This enum is the minimal status. A machine with a driver-owned program counter or external +scheduler uses a richer step outcome carrying owned control-flow and scheduling requests beside the +status. Faults remain errors unless a runtime specifically models traps as first-class state. + +`Complete` means: + +- The instruction and all immediate effect handling finished. +- The machine has reached a boundary at which its driver may select more work. +- It does not select the next instruction or imply that another instruction executes at the same + modeled time. + +`Parked` means: + +- The handler atomically registered the work needed to resume. +- No borrow from instruction execution is needed to resume. +- The driver must not treat this execution context as runnable until the corresponding owned + continuation becomes ready. + +Instruction execution remains synchronous. `send`, `recv`, external I/O, and delayed hardware +completion express suspension through effects handled after `execute` returns. + +Timing remains driver policy. The same instruction may have different modeled duration under +different drivers or configurations. Timing data may come from: + +- Driver configuration. +- Optional route metadata. +- Runtime instruction data. +- A component result. +- Resource state. +- A configured timing table. +- A child clock. +- An external completion event. + +Host execution time never determines modeled duration. + +## Runtime and Program Drivers + +A composite can execute one supplied runtime instruction, but that ability does not make it a +running system. Something must still obtain the next instruction from the configured source, +decide when it is eligible to run, interpret the result of the step, and repeat or stop. That +orchestration role is the **driver**. The source may itself be a modeled sequencer, so the driver +need not be the authority that computes the program counter. + +This document uses **runtime** for the top-level running arrangement: a composite machine, a +selected driver policy, and any resolved programs used by that policy. This is distinct from a +*runtime instruction*, which is one fully resolved operation in a program. + +### The Step/Driver Boundary + +The stable machine boundary is one instruction: + +```text +driver obtains a runtime instruction from the configured source + -> machine.step(instruction) + -> resolve runtime message + -> execute on the selected component + -> handle immediate effects + -> return an owned outcome + -> driver interprets the outcome + -> driver selects the next work, waits, or stops +``` + +The driver is necessary because none of the following has one correct policy for every vihaco +machine: + +- Whether instructions come from a stored program, an interactive caller, a device stream, or an + event queue. +- Whether successful completion advances a cursor. +- Whether a branch mutates a machine component or returns a control request to the caller. +- Whether another instruction runs immediately or at a later modeled time. +- Whether one machine runs to completion or several machines are interleaved. +- Whether a parked operation blocks the caller, yields to another machine, or is exposed as an + incomplete result. +- Whether breakpoints, tracing, deterministic replay, or external hardware completions participate + in instruction selection. + +`step` must therefore remain usable without a program counter or clock. A unit test, debugger, or +host application can construct a runtime instruction and call `step` directly. A program driver +builds repetition on top of exactly the same operation. + +Conceptually, the boundary may be expressed as: + +```rust +pub trait Step { + type Instruction; + type Outcome; + type Fault; + + fn step( + &mut self, + instruction: &Self::Instruction, + ) -> Result; +} +``` + +The associated outcome is intentionally machine-specific. A simple machine may need only +`Complete` and `Parked`; a control-flow machine may also need to communicate advance, jump, halt, +trap, or breakpoint information. The framework should not force every machine to carry control +states that it cannot produce. + +The generated dispatch is still valuable even though every match arm has the same three stages. +The runtime instruction variant selects different concrete instruction types, target fields, +message resolvers, effect handlers, and fault conversions. The driver repeats `step`; it does not +replace that route-specific dispatch. + +### Runtime Ownership + +A convenient top-level owner places the driver and machine beside one another: + +```rust +pub trait Driver { + type Output; + + fn run(&mut self, machine: &mut M) -> eyre::Result; +} + +pub struct Runtime { + pub machine: M, + pub driver: D, +} + +impl Runtime { + pub fn run(&mut self) -> eyre::Result + where + D: Driver, + { + self.driver.run(&mut self.machine) + } +} +``` + +The exact API may differ, and the first implementation may use inherent `run` methods instead of a +common `Driver` trait. The important ownership rule is that the driver is external to the +composite it drives. “External” here means that it is not a field that must borrow its containing +composite; it may still be a normal vihaco type in the same process and may be owned by a +`Runtime`. + +This sibling arrangement lets the driver hold its own mutable policy state while borrowing the +whole machine for a step. Placing the driver inside the machine would require mutably borrowing the +driver field and the containing machine at the same time whenever the driver calls `step`. It would +also make a particular execution policy part of the machine's hardware shape. + +The driver should interact with machine state through explicit machine operations. It may inspect a +program counter, fetch through a program-storage interface, drain driver-facing requests, or reset +the machine when those operations are part of the selected design. It should not depend on the +private layout of arbitrary component fields. + +### What a Driver Holds + +A driver owns the state required by its selection and progression policy. Depending on the driver, +that may include: + +- A resolved program or a reference to program storage. +- One program cursor, several cursors, or no cursor. +- Entry-point and halt state. +- Breakpoints, single-step mode, and debugger bookkeeping. +- A runnable set, event queue, modeled current time, and deterministic tie-breaking order. +- Pending external operations and the owned identifiers used to resume them. +- Reset generations used to reject stale completions. +- Replay input, recorded decisions, or a source of test instructions. + +A driver does not own component invariants or execute component operations itself. It supplies a +runtime instruction to the composite and responds to the resulting outcome. Component-local state +remains in components, and cross-component effects remain routed by the composite. + +The normal lifecycle is: + +1. Parse SST and resolve it into a runtime program. +2. Load or attach that program according to the chosen storage model. +3. Reset the machine and driver state as required. +4. Select an entry point or initial event. +5. Select a runtime instruction and call `step`. +6. Interpret completion, control flow, scheduling requests, parking, or faults. +7. Repeat, wait for a completion, or return a terminal result. + +### Driver Families + +Drivers are policies rather than a second kind of machine. Different use cases should be able to +reuse the same composite: + +| Driver | Typical state | Selection policy | +|---|---|---| +| Sequential interpreter | Program and one cursor | Run the instruction at the cursor, then advance or apply control flow | +| Single-step/debugger | Program, cursor, breakpoints, inspection state | Stop at requested boundaries and expose machine state between steps | +| Timeline/emulation driver | Global time, event queue, runnable machines, one or more cursors | Run the earliest eligible event and schedule its follow-up work | +| Cooperative multi-program driver | Programs, cursors, runnable queue | Interleave several execution contexts according to an explicit policy | +| Externally driven adapter | Pending host or device input | Execute instructions supplied by another process or hardware controller | +| Hardware completion driver | Outstanding operations and completion identifiers | Resume work in response to device completions or interrupts | +| Replay/test driver | Recorded or generated instruction decisions | Reproduce a trace or explore instruction sequences deterministically | + +These can be layered. A debugger may wrap a sequential or timeline driver. A replay facility may +record the choices of another driver. A host adapter may feed instructions to a machine that has no +stored program or program counter at all. + +The initial implementation should begin with concrete drivers needed by reference machines. A +universal driver trait is useful only if those drivers demonstrate a stable shared contract. The +one-instruction `Step` boundary is more fundamental than requiring every orchestration policy to +fit one trait immediately. + +### Drivers and Clocks + +A clock is not intrinsically a framework-wide authority. It may occupy either of two roles: + +1. An ordinary component or handler that owns local clock state, translates device ticks, records + durations, or produces scheduling requests. +2. A driver whose notion of global time determines which instruction or event executes next. + +A sequential interpreter that runs at its caller's pace may have no clock. A machine may contain a +child clock component while being driven sequentially. Conversely, a global emulation clock may be +the timeline driver and hold the event queue, current modeled time, programs, and cursors for +multiple child machines. A driver need not be a clock, and a clock need not be a driver. + +This distinction also defines how effects reach a clock. Internal components continue to handle +effects through the same typed, route-specific handling model as every other destination. An effect +may be sent deterministically to several handlers—for example, a child clock that translates a +device delay and a debug component that records it. Each handler receives the same semantic effect +in declaration order, may mutate its own component, and may emit owned follow-up effects. The +shared input need not be consumed by the first handler, and no separate handling semantics are +required merely because one handler uses the effect only for diagnostics. + +If an effect must influence the external driver, its scheduling meaning must survive the `step` +boundary as owned data. A route can accomplish that in either of two broad ways: + +- Return a driver-facing request as part of the step outcome. +- Record the request in explicitly exposed machine state that the driver drains after the step. + +Returning owned requests makes the boundary clearest, while machine-owned queues may be appropriate +when queueing is itself modeled hardware. The first implementation can choose the simpler +representation without changing the semantic rule: scheduling work intended for an external +driver cannot be consumed exclusively by an internal handler. + +A typical clock hierarchy is: + +```text +instruction emits device scheduling effect + -> route sends it to child clock and diagnostic handlers + -> child clock converts device ticks to a global scheduling request + -> step returns that owned request + -> global clock-driver inserts it into the event queue + -> driver resumes the machine when the event becomes current +``` + +The global clock does not need to see every mutation performed directly by `Execute`. It only +needs the information that affects global ordering, modeled duration, or readiness. When a direct +mutation has such consequences, the instruction result or its route must expose the relevant fact +or scheduling request. Purely component-local changes can remain local. + +Any operation that may park must make that fact visible at the driver boundary. The operation may +first emit an effect that an internal resource handles, but the resulting `Parked` outcome and owned +continuation identity must reach the driver. An instruction must not silently block inside +`Execute` or leave the driver believing that the execution context is still runnable. + +### Program and Program-Counter Placement + +Program storage, a program cursor, and the policy that advances the cursor are separate concepts. +They may be colocated for convenience, but the architecture should not require them to have the +same owner. + +| Placement | Appropriate when | Consequences | +|---|---|---| +| Program and cursor in the driver | Ordinary interpretation, debugging, replay, or several cursors over shared code | The machine receives selected instructions and need not model program storage | +| Program in the driver, cursor in the machine | The program is host-owned but the program counter is visible or mutable hardware state | The driver reads the machine cursor, fetches the instruction, and lets machine policy determine the next cursor | +| Program in the machine, cursor in the driver | Program memory is modeled or device-resident but progression is host-controlled | The driver fetches through an explicit machine operation and owns advance/jump policy | +| Program and cursor in the machine | A sequencer or control-flow unit owns both fetch state and progression | The external driver obtains the next owned instruction through the sequencer and then calls `step` | +| Program supplied externally, no cursor | Interactive execution, streaming control, tests, or a hardware command source | Each instruction is supplied directly and `step` remains fully usable | + +Resolved program contents are usually immutable and may be shared. A cursor is mutable execution +state and there may be several cursors for one program. The rewrite should therefore model program +data and per-execution cursor state as distinct concepts even if it retains a convenient +one-program/one-cursor wrapper. The existing `ProgramImage` shape can remain such a convenience, +but combining a module and one program counter must not make that placement a requirement for all +drivers. + +When the driver owns the cursor, control-flow handling returns driver-facing control such as +advance, jump, call, return, halt, or park. The driver is the sole authority that applies those +changes. It must not increment the cursor before the step and then also apply an advance outcome. + +When the machine owns the cursor, a selected control-flow component or route handler mutates it. +The machine's route policy also applies its ordinary sequential advance when no explicit +control-flow operation replaces it. The driver fetches using the current value and reads the +updated value after the step. In this arrangement the driver must not independently infer that +every completed instruction advances by one. There must be one authority for each cursor +transition. + +Machine-owned cursors are important for hardware-oriented models. A sequencer, branch unit, +interrupt controller, direct-memory-access engine, or external device may drive the program +counter. Treating the cursor as a component permits those modeled hardware operations to mutate it, +while the external driver remains responsible for deciding when the machine is allowed to perform +work. A timeline driver can therefore schedule a sequencer without pretending that the global clock +owns the sequencer's program counter. + +Rust ownership affects the fetch interface when program storage lives inside the same machine that +will be mutably stepped. A driver cannot retain a reference borrowed from the machine's program +field while also borrowing the whole machine mutably for `step`. The selected API must end the +fetch borrow before execution—for example, by returning an owned runtime instruction—or separate +immutable program storage from the mutable composite. This is a concrete ownership constraint, not +a reason to prescribe one placement for all machines. + +### Parking and Resumption + +Parking divides ownership between the machine and driver: + +- The component or resource owns the continuation data required to finish the operation. +- The composite ensures that effect handling registers that continuation atomically. +- The step outcome tells the driver that the execution context is no longer runnable. +- The driver owns when the context re-enters its runnable set. +- A completion event carries an owned identity that can be checked against resets or cancellation. + +No borrow from resolution, execution, or effect handling may survive the step. A simple sequential +driver that does not wait for asynchronous work may return `Parked` to its caller. A timeline driver +may keep running other machines until the relevant event is ready. A hardware driver may wait for +an external completion and then resume the registered continuation. These are different driver +policies over the same machine boundary. + +### Consequences for the Rewrite + +The rewrite should establish these pieces in order: + +1. Generate a one-runtime-instruction `step` operation for each composite. +2. Make its outcome sufficient for a caller to distinguish completion, parking, terminal control, + and driver-facing work required by the reference machine. +3. Implement a simple sequential driver without requiring a clock. +4. Keep program data and cursor state conceptually separate, with an initial convenient ownership + arrangement. +5. Add a timeline driver in which the global clock owns selection and scheduling policy. +6. Demonstrate a machine-owned program counter so hardware-driven progression does not become an + afterthought. +7. Generalize a common driver trait only after these concrete drivers reveal the shared API. + +This division keeps component execution reusable while allowing each runtime to decide what +“next,” “now,” and “runnable” mean. + +## Atomicity and Faults + +Atomicity means that another instruction from the same machine does not interleave with the current +step. It does not imply rollback. Every step reaches one of three boundaries: + +- Complete. +- Parked with a registered continuation. +- Faulted. + +Message resolution may consume operands before execution, and the selected component may mutate +itself before returning a fault. A terminal fault may therefore leave partially consumed or +mutated state. + +Avoiding automatic rollback prevents the common path from cloning values solely to recover from a +fault. An operation that requires transactional semantics implements them explicitly in its owning +component or resource. + +Each route documents the relevant failure boundary: + +- Whether operands are read or consumed. +- Which mutations may occur before a fault. +- Whether effect handling itself can fault. +- Whether a parked operation is cancellable. +- What happens to stale completions after reset. diff --git a/vision/sst-resolution.md b/vision/sst-resolution.md new file mode 100644 index 00000000..708fcb3b --- /dev/null +++ b/vision/sst-resolution.md @@ -0,0 +1,155 @@ +# SST Parsing and Resolution + +The pattern parser is the single SST syntax frontend. It constructs surface instructions and never +runtime instructions: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(instruction, head = "control")] +#[pattern = "'conditional_branch `@` $when_true `,` `@` $when_false"] +pub struct SurfaceConditionalBranch { + pub when_true: String, + pub when_false: String, +} +``` + +The generated parser accepts: + +```text +control::conditional_branch @then, @otherwise +``` + +The surface instruction owns its `head`, written without a trailing `::`, and its first pattern +atom is the instruction token. + +Pattern compilation validates the complete mapping from syntax to the Rust product: + +- Tuple instructions use numeric bindings such as `$0`. +- Named instruction structs use field bindings such as `$value`. +- Every field is bound exactly once. +- Bindings may be reordered without changing constructor field order. +- Unit instructions contain no bindings. +- Instruction patterns begin with one mnemonic token. +- Leading, trailing, repeated, and tab-separated pattern whitespace is rejected. +- Literal keywords are written as backtick atoms. +- Comma and `@` have punctuation-aware literal forms. + +Every bound field delegates to that field type's +`vihaco_parser_core::Parse::parser()`. Specialized syntax belongs in a surface type with its own +pattern-derived parser: + +1. Use a local type with an appropriate `Parse` implementation. +2. Use a local newtype around a foreign type. +3. Parse through a richer surface instruction and lower it during `Resolve`. + +A missing language construct is addressed by extending the pattern generator, not by introducing a +second parser or compatibility attribute system. + +## Value and Type Operands + +The `value` and `type` syntax classes let instruction fields delegate grammar to domain types: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(type)] +#[pattern = "`i64`"] +pub struct I64Type; + +#[derive(vihaco_parser::Parse)] +#[syntax_class(value)] +#[pattern = "$value"] +pub struct ImmediateI64 { + pub value: i64, +} +``` + +Value and type patterns cannot contain instruction tokens. Types always declare an explicit +pattern. Values receive defaults only for unambiguous unit or single-field forms; multi-field +values declare their pattern explicitly. + +The instruction pattern consequently remains structural: it binds fields, while each field type +owns its grammar. + +## Canonical Syntax Ownership + +A reusable surface instruction owns its canonical dialect head and pattern. The composite decides +whether to admit that parser, but does not normally rewrite its mnemonic or head. + +The composite may still provide: + +- Source-symbol aliases for a child section or device. +- Pattern-derived sugar forms that lower to one or more runtime instructions. +- Compatibility aliases represented by explicit surface instruction types. +- Machine-local wrappers when a genuinely different public syntax is required. + +These are source-orchestration concerns rather than mutations of a canonical pattern. Two unrelated +spellings use two surface instruction types, or an explicit source-level enum, even when both lower +to the same runtime operation. + +## Composite Parser Generation + +The machine surface parser is the sum of exactly the selected surface products: + +```text +Parse + + Parse + + Parse + = Parse +``` + +Omitted surface instructions are absent from the parser choice by construction. The composite does +not parse every component catalog and reject unsupported forms afterward; unsupported SST is +unrecognizable at the parser boundary. + +The pattern parser composes the selected alternatives, including overlapping mnemonic prefixes and +large instruction sets. The composite supplies types and does not implement a separate parsing +algorithm. + +## Parsing Versus Resolution + +Pattern parsing and module resolution are consecutive but distinct boundaries. Parsing always +constructs a surface instruction. `Resolve` then uses module-wide +context to construct runtime instructions: + +- Labels and symbolic branch targets require symbol resolution. +- Interned strings require a module interner. +- Sugar may expand one surface instruction into several runtime instructions. +- Overloaded forms can be separate surface instruction types. +- Machine-specific validation may depend on headers or other section metadata. + +The full distinction is: + +```text +pattern parsing: + source text -> surface instruction + +module resolution: + ParsedModule + -> Resolve + -> Module + +runtime message resolution: + runtime instruction + machine state -> Execute::Message +``` + +For `ConditionalBranch`, parsing preserves `@then` and `@otherwise` as source names. Module +resolution replaces them with `usize` program indices. Runtime message resolution may later obtain +the condition from the operand stack, but never resolves the labels again. + +## Naming the Three Instruction Concepts + +The API needs distinct names for three different concepts: + +- A surface instruction. +- A runtime operation. +- A generated machine runtime-instruction sum. + +A consistent naming direction is: + +- `SurfaceInstruction` for the types constructed by the pattern parser. +- `Instruction` for an individual runtime operation. +- `MachineInstruction` or `InstructionSet` for the generated runtime sum. +- `Resolve` for module lowering. + +The exact identifiers remain an API decision; the three roles must remain visible. + diff --git a/vision/stack-machine-policy.md b/vision/stack-machine-policy.md new file mode 100644 index 00000000..5da0e3b2 --- /dev/null +++ b/vision/stack-machine-policy.md @@ -0,0 +1,118 @@ +# Stack Machine Policy + +The stack machine makes the state-ownership rule concrete. Containing a stack does not grant every +instruction direct push and pop access. Stack-owned operations mutate the stack; operations owned +elsewhere cross the composite through messages and effects: + +> Native stack operations mutate their selected stack directly. Operations owned by another domain +> obtain stack inputs through message resolution and return stack outputs through effect handling. + +## Native Stack Instructions + +Operations whose semantics are entirely stack-local naturally target the stack component: + +- Push an immediate value. +- Drop a value. +- Duplicate a value. +- Swap or rotate values. +- Perform an invariant-preserving stack-local load/store if the stack owns those slots. + +The composite still selects each operation explicitly. A method on `Stack` does not become a +program instruction by existing. + +## Arithmetic + +Reusable arithmetic receives values rather than access to their storage: + +```text +resolve: + consume lhs and rhs from operand_stack + +execute on arithmetic component: + produce lhs + rhs + +handle: + push the result onto operand_stack +``` + +A fused `stack::Add` remains a valid architecture-specific operation: + +- It is simpler and may be faster. +- It can preserve stack-specific atomicity. +- It is less reusable in register or expression-tree machines. +- Its internal stack mutation is less visible to diagnostic handlers. + +Both forms may coexist under distinct names or modules. Their difference is architectural rather +than ergonomic: one isolates arithmetic semantics, while the other owns an entire stack +transition. + +## Locals and Loads + +Separate local and operand storage uses the staged path: + +```text +resolve: + read or consume the local value from locals + +execute: + validate or transform the value if needed + +handle: + push the result to operand_stack +``` + +When one stack component owns both operand and local-frame semantics, a component-local `Load` may +mutate it directly. State ownership, rather than the instruction's spelling, determines the route. + +## Heap Allocation + +Heap allocation crosses component boundaries because the heap owns allocation while the stack owns +its operands and result: + +```text +resolve: + consume N values from operand_stack + +execute on heap: + allocate values and produce HeapReference + +handle: + push HeapReference onto operand_stack +``` + +The heap preserves allocation invariants, and the composite preserves the machine's dataflow. + +## Printing + +Printing separates value acquisition from external output: + +```text +resolve: + read or consume the selected value + format or resolve strings according to machine policy + +execute: + produce PrintEffect with owned text + +handle: + deliver to stdout, a diagnostic handler, or a scheduled I/O resource +``` + +The route resolver determines whether printing reads or consumes the stack value. + +## Calls and Control Flow + +Control flow emits nominal effects such as `JumpTo`, `Invoke`, and `ReturnFromCall`. The configured +program-counter placement determines their destination: + +- With a machine-owned program counter, a route handler applies control effects to the selected + program-counter component. +- With a driver-owned program counter, the route returns equivalent driver-facing control. +- Call-stack selection, frame construction, and return-value placement remain composite routing + concerns because they cross component boundaries. +- Timing and selection of the next runnable instruction remain driver concerns, although handlers + may produce the information used for those decisions. + +This makes control-flow instructions reusable across different program, cursor, frame, and driver +representations without allowing both the machine and driver to advance the same cursor. + diff --git a/vision/traits.md b/vision/traits.md new file mode 100644 index 00000000..a8818037 --- /dev/null +++ b/vision/traits.md @@ -0,0 +1,56 @@ +# Vihaco Traits + +## Objective + +We need to make vihaco ideas - instructions, messages, message resolution, effects, and effect handlers - +first class Rust traits and provide the supporting scaffolding for moving between each stage in the vihaco +framework. + +## Instructions + +### Introduction + +Currently, instruction sets are defined using a single Rust enum. This works well for vihaco currently, but becomes +limiting when we think about the framework as a) providing composable and reusable components, and b) becoming a compilation +target for composite DSLs: + +1. We lose information about the specific messages an instruction will need and what effect(s) an instruction will + emit; +2. We don't know the state an instruction will need to access or mutate from its execution environment; +3. Instructions are locked into a specific instruction set, and instructions with identical logic will need to be + implemented twice. + +We will introduce a new instruction trait: + +```rust +/// A single instruction. +/// +/// An instruction receives its [`State`] as a type parameter with capabilities +/// declared. Each capability describes some action that the instruction requires +/// from the external environment. +trait Instruction { + /// The information needed by the instruction that it doesn't have inline. + type Message: Message; + + /// The information that exits an instruction and is dispatched to its + /// handler. + type Effect: Effect; + + fn execute( + &self, + ctx: &mut S, + msg: Self::Message + ) -> Result, S::Error>; +} +``` + +This will solve the above problems by: + +1. Requiring that instructions declare their message and effect as associated types; +2. Requiring the instruction to declare the necessary state and capabilities it needs; +3. Making instructions individual structs that can `impl Instruction` while still being grouped + by an instruction set enum for cheap dispatch. + +### Defining an Instruction Set + +We will make use of an `instruction_set!` proc macro diff --git a/vision/vision.md b/vision/vision.md new file mode 100644 index 00000000..5f0d3947 --- /dev/null +++ b/vision/vision.md @@ -0,0 +1,240 @@ +## Objective + +As we begin building on top of vihaco and use it as a compilation target: + +1. We should strive to take advantage of Rust's high level features. The goal is to take common vihaco ideas currently supported by macros - messages, effects, instructions, effect observers, etc. - and move them into the type system where possible. This will allow DSL output + +### Composite DSL + +### Instruction Set DSL + +### Dispatch Loop + +- vihaco needs to own + +### Capability Traits + +- include justification for the idea of capability traits with examples + - instruction has two capabilities that perform on a stack; some use cases might use the same + stack, different stack, etc. + + I might want: + ```rust + struct LoadContext<'a> { + get: &'a Stack, + push: &'a mut Stack + } + ``` + + but Rust won't let me use the same stack for get and push because of mutable borrow rules; we + need to allow for same stack, different stack, etc. capability traits let us do that +- abstraction across runtimes, using multiple contexts for the same instruction, + allowing single runtime to impl same trait many times, etc. + +### First Class `vihaco` Traits + +Instructions + +```rust +/// A single instruction. +/// +/// An instruction receives its [`State`] as a type parameter with capabilities +/// declared. Each capability describes some action that the instruction requires +/// from the external environment. +trait Instruction { + /// The information needed by the instruction that it doesn't have inline. + type Message: Message; + + /// The information that exits an instruction and is dispatched to its + /// handler. + type Effect: Effect; + + fn execute( + &self, + ctx: &mut S, + msg: Self::Message + ) -> Result, S::Error>; +} +``` + +Message Resolution + +```rust +trait ResolveMessage: State + Sized +where + I: Instruction, +{ + fn resolve(&mut self, inst: &I) -> Result; +} + +impl ResolveMessage for T +where + T: State + Sized, + I: Instruction, +{ + #[inline(always)] + fn resolve(&mut self, _inst: &I) -> Result { + Ok(NoMessage) + } +} +``` + +Effect Handling +```rust +trait ResolveMessage: State + Sized +where + I: Instruction, +{ + fn resolve(&mut self, inst: &I) -> Result; +} + +impl ResolveMessage for T +where + T: State + Sized, + I: Instruction, +{ + #[inline(always)] + fn resolve(&mut self, _inst: &I) -> Result { + Ok(NoMessage) + } +} +``## Instructions + +We are going to replace the grouping of instruction sets by enums into individual `impl Instruction` +on Rust structs: + +```rust +trait Instruction { + type Message; + type Result; + type Fault; + + fn execute( + &self, + msg: Self::Message + ) -> Result; +} +``` + +We will continue with the idea of an instruction having three stages: + +1. **Message Resolution**: What does this instruction need from its execution information? +2. **Instruction Execution**: How does the instruction execute? +3. **Effect Handling**: What effect does this instruction have on its environment? + +Moving instructions into their own individual instructions allows for vihaco to know, statically, +what information moves in and out of each instruction. If we were to have a single execute +instruction that matches over variants of an enum, we can construct impossible combinations of +messages and instructions. By moving vihaco ideas into Rust's type system, we can statically ensure +that the information an instruction has during its execution is correct *by construction*. + +### Instruction Stepping + +The barebones representation of a single instruction's entire pipeline is modeled below: + +```rust +fn step(instruction: &I, state: &mut S) -> Result +where + I: Instruction, + S: ResolveMessage + Handle, + S::Error: From, +{ + let msg = state.resolve(instruction)?; + let result = instruction.execute(msg)?; + state.handle(result) +} +``` + +This matches exactly with the three stages of an instruction. + +### Message Resolution + +Message resolution for a specific instruction is dictated through a trait that the machine +implements. This way, instructions stay the same across machines, but the way the message +is resolved can vary based on the encompassing runtime. + +```rust +trait ResolveMessage: State + Sized +where + I: Instruction, +{ + fn resolve(&mut self, inst: &I) -> Result; +} +``` + +Not all instructions require instructions, so we provide a blanket implementation for +`ResolveMessage` for `Instruction`: + +```rust +impl ResolveMessage for T +where + T: State + Sized, + I: Instruction, +{ + #[inline(always)] + fn resolve(&mut self, _inst: &I) -> Result { + Ok(NoMessage) + } +} +``` + +Message resolution is provided for instructions that need more information from their runtime +environment before they can execute. Think of a `Print` instruction: + +```rust +struct Print { + string: usize, +} +``` + +The `Print` instruction might require that strings are interned by the loader before program execution +during module resolution, meaning that it only has a `usize` index into a string intern table. + +### Support for Asynchronous Instructions + +We are going to enforce **instruction boundary suspension**, meaning that an instruction can only perform +a suspending operation in tail position. In vihaco, this will come in the form of an effect, as `Instruction` +requires that `execute` is synchronous. This comes as a + +Take a hypothetical `recv` example: + +```rust +struct Receive { + channel: ChannelId, +} + +impl Instruction for Receive { + /* associated types */ + + fn execute( + &self, + msg: Self::Message + ) -> Result { + /* execution body */ + } +} +``` + + +```rust +enum Execution { + Complete, + Parked, +} +``` + +```rust +fn step(instruction: &I, state: &mut S) -> Result +where + I: Instruction, + S: ResolveMessage + Handle, + S::Error: From, +{ + let msg = state.resolve(instruction)?; + let result = instruction.execute(msg)?; + state.handle(result) +} +``` + +--- +` From 9eee85ee6234463fd779cb4a5b9f7b366cfd944b Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 28 Jul 2026 16:27:45 -0400 Subject: [PATCH 02/15] Added vision for types and values --- vision/contents.md | 17 +- vision/demo.md | 13 +- vision/design-tradeoffs.md | 24 ++ vision/execution-pipeline.md | 64 ++-- vision/implementation-plan.md | 91 ++++- vision/instruction-model.md | 63 ++-- vision/macro-generation.md | 33 +- vision/sst-resolution.md | 39 +- vision/stack-machine-policy.md | 12 +- vision/types-and-values.md | 657 +++++++++++++++++++++++++++++++++ 10 files changed, 927 insertions(+), 86 deletions(-) create mode 100644 vision/types-and-values.md diff --git a/vision/contents.md b/vision/contents.md index 2321c121..8fb3d0cf 100644 --- a/vision/contents.md +++ b/vision/contents.md @@ -12,19 +12,22 @@ runtime execution: 1. [`instruction-model.md`](./instruction-model.md) defines surface and runtime instruction products, `Instruction` and `Execute`, component responsibilities, composite selection, and generated machine instruction sums. -2. [`execution-pipeline.md`](./execution-pipeline.md) defines surface resolution and the +2. [`types-and-values.md`](./types-and-values.md) defines author-owned data models, scalar parser + and encoding support, surface/runtime type and value staging, cross-component compatibility, + explicit conversion, and future bytecode encoding. +3. [`execution-pipeline.md`](./execution-pipeline.md) defines surface resolution and the route-specific runtime stages of message resolution, component execution, and effect handling. -3. [`runtime-drivers.md`](./runtime-drivers.md) defines step outcomes, program drivers, clock and +4. [`runtime-drivers.md`](./runtime-drivers.md) defines step outcomes, program drivers, clock and driver roles, program-counter ownership, parking, resumption, and fault boundaries. -4. [`stack-machine-policy.md`](./stack-machine-policy.md) applies the ownership model to native +5. [`stack-machine-policy.md`](./stack-machine-policy.md) applies the ownership model to native stack operations, arithmetic, locals, heap allocation, printing, calls, and control flow. -5. [`sst-resolution.md`](./sst-resolution.md) defines pattern-based SST parsing, surface-to-runtime +6. [`sst-resolution.md`](./sst-resolution.md) defines pattern-based SST parsing, surface-to-runtime resolution, canonical syntax ownership, and the generated composite parser. -6. [`macro-generation.md`](./macro-generation.md) separates what instruction, component, +7. [`macro-generation.md`](./macro-generation.md) separates what instruction, component, composite, and effect-wiring macros generate from what machine authors write. -7. [`design-tradeoffs.md`](./design-tradeoffs.md) records the alternatives considered and the +8. [`design-tradeoffs.md`](./design-tradeoffs.md) records the alternatives considered and the architecture's observability, debugging, and error-model consequences. -8. [`implementation-plan.md`](./implementation-plan.md) defines test coverage, migration phases, +9. [`implementation-plan.md`](./implementation-plan.md) defines test coverage, migration phases, focused architecture fixtures, deferred questions, and acceptance criteria. ## Reference Machine and Timing diff --git a/vision/demo.md b/vision/demo.md index 3d6aad34..3e881134 100644 --- a/vision/demo.md +++ b/vision/demo.md @@ -84,6 +84,12 @@ of a particular mailbox, interconnect, clock, stack, or arithmetic implementatio ship with the vihaco project as useful libraries, but the framework must not contain special cases for their names or semantics. +The first demo uses `i64` directly for its operand stacks, arithmetic messages/results, and channel +payloads. It does not need a heterogeneous value enum or runtime type descriptor. This is a +deliberate acceptance case for the author-defined data-model boundary: adding the demo must not +reintroduce a vihaco `Value` or `Type`. Focused heap or mixed-value fixtures may define their own +carrier independently. See [`types-and-values.md`](./types-and-values.md). + The core requirement is more general: - A nested composite can emit an owned effect across its parent boundary. @@ -128,7 +134,8 @@ handle: The arithmetic component does not know which CPU contains it, which stack supplied the values, or how long a local cycle lasts globally. The same instruction and component implementations execute -in both CPUs. +in both CPUs. In the first demo their shared boundary type is `i64`, so no cross-component cast is +performed. Each arithmetic route initially costs one local cycle. Because the local clocks have different ratios, the same semantic operation has different global duration: @@ -380,7 +387,7 @@ The demo should be assembled from reusable items rather than defining all behavi example: - A stack component with invariant-preserving operations. -- Arithmetic runtime instructions and an arithmetic component implementing `add`, `sub`, and +- Arithmetic runtime instructions and an `i64` arithmetic component implementing `add`, `sub`, and `mul`. - Surface instruction types and resolution support for those arithmetic operations. - A local clock component with a configurable local-cycle-to-global-tick ratio. @@ -428,6 +435,8 @@ The demo is complete when: - Global time is monotonic and same-tick ordering is deterministic. - Arithmetic touches only each CPU's local stack. - Values cross CPUs only through typed effects and library-defined communication handlers. +- Arithmetic and communication share `i64` directly; no framework value enum or implicit cast is + involved. - `recv` parks when no value is available and resumes without retaining a borrow. - A parked CPU does not execute its next instruction. - Both SST programs lower entirely to the selected runtime instruction sums. diff --git a/vision/design-tradeoffs.md b/vision/design-tradeoffs.md index ac583145..332861a5 100644 --- a/vision/design-tradeoffs.md +++ b/vision/design-tradeoffs.md @@ -96,6 +96,30 @@ This is the default because it places each responsibility at the narrowest stabl boundary. Pure operations remain available through stateless executors, and cross-component operations deliberately use message and effect staging. +## Type and Value Ownership + +A framework-owned `Value` enum would make heterogeneous stacks convenient, but it would turn every +machine's guest data model into a vihaco compatibility decision. Component-owned value universes +have the opposite problem: values crossing a stack, arithmetic unit, heap, or channel boundary +would lose one shared identity or require pervasive adapters. + +The selected model leaves semantic values and types with machine and library authors: + +- Vihaco supplies scalar parsing, generic containers, and byte-codec infrastructure. +- Libraries define reusable domain products such as heap or channel identifiers. +- A machine author may define a closed carrier when its architecture requires one. +- Components are generic over, or explicitly support, those products. +- A composite selects concrete compatible instantiations without generating a universal carrier. + +This supports both `Stack` and `Stack` without privileging either architecture. +It also lets multiple composites share one data-model crate. + +Cross-component wiring is exact by default. Automatically casting mismatched message and effect +types would hide whether conversion is checked, saturating, wrapping, lossy, or a bit +reinterpretation. Resolution may insert a conversion defined by the source language, but the +resolved runtime path records that choice explicitly. See +[`types-and-values.md`](./types-and-values.md). + ## Observability and Debugging Direct component mutation means not every state change naturally appears in the effect stream. The diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md index 72f9f47f..91b07ae1 100644 --- a/vision/execution-pipeline.md +++ b/vision/execution-pipeline.md @@ -11,30 +11,33 @@ SST loading follows this path: ```text SST text -> pattern parser - -> ParsedModule - -> Resolve - -> Module + -> ParsedModule + -> Resolve + -> Module -> runtime program image ``` -`Resolve` owns every transformation that requires module-wide source -context: +`SurfaceType`, `Constant`, and `RuntimeType` are author-defined products rather than vihaco enums. +`Resolve` owns every transformation that requires +module-wide source context: - Building and consulting label tables. -- Turning `@label` references into `usize` program indices. +- Turning `@label` references into fixed-width `InstructionIndex` values. - Interning strings. - Expanding surface sugar into one or more runtime instructions. - Validating source-level types and declarations. +- Interpreting typed or unresolved author-defined literals. +- Selecting explicit conversions required by the source language. At the trait boundary, resolution consumes a parsed surface module and produces a runtime module: ```rust -pub trait Resolve { +pub trait Resolve { type Module; fn resolve_module( &mut self, - parsed: ParsedModule, + parsed: ParsedModule, ) -> eyre::Result; } ``` @@ -111,9 +114,10 @@ but that is an explicit machine configuration rather than universal step behavio ### Stage 1: Message Resolution Runtime message resolution supplies the owned, execution-time information that is intentionally -absent from the instruction. It is distinct from `Resolve`: module -resolution transforms parsed source into a runtime program, while message resolution reads live -machine state for an instruction that is already fully resolved. +absent from the instruction. It is distinct from +`Resolve`: module resolution transforms parsed source +into a runtime program, while message resolution reads live machine state for an instruction that +is already fully resolved. The composite route owns this stage because only the composite knows: @@ -295,8 +299,9 @@ outer machine instruction; it is not resolved globally from `Effect`. ##### Resolution Selects the Runtime Route The composite declaration defines the available runtime routes, and the composite macro gives each -one a machine instruction variant. `Resolve` selects among those -variants while lowering surface instructions into the runtime module. +one a machine instruction variant. +`Resolve` selects among those variants +while lowering surface instructions into the runtime module. This separation allows one SST operation to select a machine-specific execution path after its source operands are resolved. A typed addition illustrates the distinction: @@ -306,10 +311,13 @@ source operands are resolved. A typed addition illustrates the distinction: #[syntax_class(instruction, head = "arithmetic")] #[pattern = "'add $ty"] pub struct SurfaceAdd { - pub ty: SurfaceType, + pub ty: ArithmeticSurfaceType, } ``` +`ArithmeticSurfaceType` is supplied by the arithmetic or machine data-model author. It is not a +vihaco core type. + The same surface product parses both of these forms: ```text @@ -351,10 +359,10 @@ fn resolve_add( instruction: SurfaceAdd, ) -> eyre::Result { match self.resolve_type(instruction.ty)? { - RuntimeType::Integer => Ok(MyMachineInstruction::IntegerAdd( + ArithmeticType::Integer => Ok(MyMachineInstruction::IntegerAdd( arithmetic::runtime::Add, )), - RuntimeType::Address => Ok(MyMachineInstruction::AddressAdd( + ArithmeticType::Address => Ok(MyMachineInstruction::AddressAdd( arithmetic::runtime::Add, )), ty => Err(eyre::eyre!("addition is not supported for {ty}")), @@ -376,6 +384,17 @@ The parser does not select a component, and `Execute` does not inspect the one. Resolution makes that architectural decision once, while it has source and type context. The resulting runtime route then carries the decision through execution and effect handling. +##### Type Compatibility and Conversion + +Route wiring preserves Rust boundary types. Moving an effect or message from one component to +another does not trigger a cast. A route whose producer emits `i64` cannot target a handler that +requires `f64` unless the author selects a conversion instruction, component, or named handler. + +When the source language defines an implicit coercion, the module resolver makes it explicit in the +runtime program or converts a source constant during resolution. Checked, saturating, wrapping, +lossy, and bitwise conversions remain distinct operations. Generated dispatch and effect forwarding +never choose conversion semantics. + ##### Same Effect Type, Different Machine Semantics The same `Add` runtime instruction can therefore appear through two routes: @@ -394,15 +413,16 @@ runtime_instructions { } ``` -Both routes contain the same runtime instruction type and produce `ValueResult`, but they +Both routes contain the same runtime instruction type and produce +`ValueResult`, where `MachineValue` is an illustrative author-defined carrier. They execute on different component instances and apply their effects to different stacks: ```text -IntegerAdd -> ValueResult -> operand_stack -AddressAdd -> ValueResult -> address_stack +IntegerAdd -> ValueResult -> operand_stack +AddressAdd -> ValueResult -> address_stack ``` -A single `Handle> for MyMachine` implementation cannot distinguish these +A single `Handle> for MyMachine` implementation cannot distinguish these policies. The effect type intentionally describes the semantic result—an operation produced a value—without naming a destination in the composite. Adding the destination to `ValueResult` would couple the arithmetic component to a particular machine layout. Replacing it with a machine-wide @@ -467,7 +487,7 @@ implementations for `IntegerAdd` and `AddressAdd` are equivalent to: ```rust impl HandleEffects for MyMachine { - type Effect = ValueResult; + type Effect = ValueResult; type Error = MachineFault; fn handle_effects( @@ -482,7 +502,7 @@ impl HandleEffects for MyMachine { } impl HandleEffects for MyMachine { - type Effect = ValueResult; + type Effect = ValueResult; type Error = MachineFault; fn handle_effects( diff --git a/vision/implementation-plan.md b/vision/implementation-plan.md index b0fd2ef1..1731d87e 100644 --- a/vision/implementation-plan.md +++ b/vision/implementation-plan.md @@ -1,4 +1,4 @@ -# Instruction Rewrite Verification and Migration +# Instruction and Data-Model Rewrite Verification and Migration This document turns the architecture into test coverage, migration phases, implementation questions, and acceptance criteria. @@ -20,18 +20,32 @@ Each surface instruction is tested for: - Preservation of unresolved names, labels, and symbolic operands. - Invalid source syntax rejection. +### Surface Value and Type Tests + +Author-defined value and type products are tested for: + +- Composition from vihaco's scalar and lexical parsers. +- Module parameter and return types using the author-selected surface type. +- Typed literal variants rejecting invalid type/literal pairings where the grammar expresses the + pairing. +- Unresolved literal text preserving the source needed by resolution. +- Out-of-range scalar input returning a parse error without panicking. +- A surface product participating in parsed modules without implementing runtime bytecode traits. + ### Resolution Tests -Each `Resolve` implementation is tested for: +Each `Resolve` implementation is tested for: - Successful lowering to the expected runtime instruction or instruction sequence. - Label and symbol replacement with the correct program-image indices. - Errors for missing, duplicate, or invalid targets. - Sugar expansion order. - Machine-specific validation that requires module context. +- Author-defined surface type and literal lowering. +- Explicit source-language conversion insertion. The `ConditionalBranch` reference case anchors the boundary: `@foo` survives parsing as a source -label and becomes a `usize` program index only during module resolution. +label and becomes a fixed-width `InstructionIndex` only during module resolution. ### Runtime Instruction Tests @@ -74,10 +88,11 @@ Compile-fail coverage proves that invalid relationships cannot be generated. It - Missing message wiring. - Missing effect handlers. - Incompatible message or effect types. +- Cross-component value types that differ without an explicit adapter. - A suspending effect without a continuation-capable handler. - A selected surface instruction that does not implement `Parse`. -- A machine surface sum with no applicable `Resolve` - implementation. +- A machine surface sum with no applicable + `Resolve` implementation. - Attempting to route a surface instruction directly to component execution. - Invalid pattern field mappings and unsupported pattern literals. @@ -97,23 +112,30 @@ End-to-end machines cover: - A nested composite exposing only selected operations. - Pattern parsing into a surface instruction, module resolution into a runtime instruction, and runtime message resolution before execution. +- One machine using a scalar directly without defining a value enum. +- One author-defined heterogeneous value carrier crossing stack, heap, and channel boundaries. ## Migration Plan Migration proceeds from the semantic relationships outward. Manual instruction and execution types establish the model first; generation follows only after the required relationships are concrete. -### Phase 1: Establish the Two Instruction Levels +### Phase 1: Establish Surface, Runtime, and Data-Model Boundaries 1. Establish distinct surface and runtime instruction types. 2. Decide the final names for surface instructions, runtime instructions, and their generated machine sums. -3. Use the pattern parser generator for all instruction, value, and type surface syntax. -4. Make `Resolve` the explicit lowering boundary. -5. Add a reference branch instruction whose surface form contains labels and whose runtime form - contains resolved `usize` program indices. -6. Test that the generated machine surface sum resolves into a module containing only variants from - the generated runtime sum. +3. Remove vihaco's built-in guest `Value` and `Type` enums. +4. Provide fallible `Parse` implementations for the supported scalar source forms. +5. Distinguish identifier, symbol, quoted-string, and unresolved-literal helpers. +6. Parameterize parsed function signatures over an author-selected surface type. +7. Keep the surface-instruction marker independent of runtime instruction/bytecode traits. +8. Use the pattern parser generator for all instruction, value, and type surface syntax. +9. Make `Resolve` the explicit lowering boundary. +10. Add a reference branch instruction whose surface form contains labels and whose runtime form + contains resolved `InstructionIndex` values. +11. Test that the generated machine surface sum resolves into a module containing only variants + from the generated runtime sum and author-defined constant/type products. ### Phase 2: Introduce Per-Instruction Component Execution @@ -165,10 +187,12 @@ establish the model first; generation follows only after the required relationsh 4. Move special field grammars into local value/type syntax types where practical. 5. Represent sugar, interning inputs, labels, and other unresolved operands explicitly in surface instruction types. -6. Implement `Resolve` to lower those forms into executable runtime instructions. -7. Move component-local mutations to `Execute` implementations. -8. Move cross-component reads into runtime message resolution. -9. Move cross-component writes and scheduling into effect handling. +6. Replace old `Value`/`Type` dependencies with scalars, generics, library newtypes, or an + author-defined data model as appropriate. +7. Implement `Resolve` to lower those forms into executable runtime instructions. +8. Move component-local mutations to `Execute` implementations. +9. Move cross-component reads into runtime message resolution. +10. Move cross-component writes and scheduling into effect handling. ### Phase 7: Remove Automatic Instruction Inheritance @@ -177,6 +201,25 @@ establish the model first; generation follows only after the required relationsh 3. Deprecate the component-wide `GeneratedComponent::Instruction` association. 4. Remove adapters after downstream code and documentation have migrated. +### Phase 8: Establish Resolved Bytecode Encoding + +1. Separate surface parsing traits from runtime encoding and decoding traits. +2. Implement portable codecs for supported fixed-width scalars and generic containers. +3. Preserve one global context and the recursive section frame, local header, local payload, child + table, and child-offset structure. +4. Add author-defined codec coverage for one scalar-only section and one heterogeneous data-model + section in the same file. +5. Generate explicit stable route opcodes scoped to each section's machine runtime-instruction sum. +6. Decide whether section schema identities live in fixed framing or author headers, and test + mismatches at the section path that selected the decoder. +7. Encode variable-sized local instruction records with checked lengths and exact payload + consumption. +8. Validate unique expected child names, parent-relative offsets, containment, and non-overlap. +9. Reject `usize`, implicit Rust discriminants, invalid tags, invalid indices, and trailing payload + data at the wire boundary. +10. Prove that recursive SST resolution and bytecode decoding establish equivalent per-section + invariants. + ## Additional Architecture Coverage [`demo.md`](./demo.md) is the only end-to-end reference runtime. It exercises nested composites, @@ -213,6 +256,8 @@ Several API choices depend on evidence from the first implementation: 6. Whether canonical dialect heads are always fixed by surface instruction types or may be wrapped by an explicit machine-local surface instruction type. 7. Which validation belongs in pattern parsing and which belongs in `Resolve`. +8. Whether repeated author data-model parameters justify a common packaging trait. +9. Whether generic tooling eventually requires self-describing type schemas in bytecode. Borrow-specific APIs and macro shorthand follow the same rule: they are introduced in response to concrete compiler friction or repeated boilerplate, not as prerequisites for the architecture. @@ -222,7 +267,8 @@ These questions do not change the central ownership decision: > Components own their state and per-instruction execution; composites own instruction admission, > route dispatch, cross-component dataflow, and effect routing; drivers own program iteration, > readiness, scheduling, and modeled time; either a driver or one modeled component owns -> program-counter transitions. +> program-counter transitions. Data-model authors own semantic values and types; vihaco supplies +> scalar, staging, composition, and encoding infrastructure. ## Acceptance Criteria @@ -238,8 +284,13 @@ The rewrite has established the architecture when all of the following are true: - The generated machine parser admits only selected surface instruction patterns. - Pattern parsing, `Resolve`, runtime message resolution, execution, and effect handling remain distinct stages. +- Vihaco exports no required guest `Value` or `Type` enum. +- Parsed function signatures use an author-selected surface type. +- A scalar-only machine does not need to define a value enum. +- Author-defined heterogeneous values can cross compatible component boundaries. +- Mismatched boundary types require an explicit conversion instruction, adapter, or handler. - A surface `ConditionalBranch` can contain `@foo`, while its runtime counterpart contains only a - resolved `usize` program index. + resolved fixed-width `InstructionIndex`. - Runtime instructions contain no unresolved source labels, names, or sugar. - Only runtime instructions are dispatched to components. - Native stack mutation requires no artificial self-directed effect. @@ -257,3 +308,7 @@ The rewrite has established the architecture when all of the following are true: - Compile errors identify the route and missing component/message/effect relationship. - Existing diagnostic-handler and loader concepts can integrate without becoming the semantic owner of instruction execution. +- Bytecode round trips author-defined instructions, constants, and types without depending on Rust + layout, variant order, or pointer width. +- One bytecode file can load a root composite and heterogeneous nested sections whose owners use + different instruction, constant, type, header, and opcode schemas. diff --git a/vision/instruction-model.md b/vision/instruction-model.md index 28d3eea0..f7992a23 100644 --- a/vision/instruction-model.md +++ b/vision/instruction-model.md @@ -23,8 +23,10 @@ The model has the following properties: - Surface instructions describe SST syntax and are parsed exclusively by the pattern parser. - Runtime instructions contain fully resolved operands and are the only instructions executed by components. -- The machine's `Resolve` implementation lowers surface instructions - into runtime instructions before execution. +- Values and type descriptors are supplied by machine and library authors; vihaco core does not + impose a guest `Value` or `Type` enum. +- The machine's `Resolve` implementation lowers surface + instructions and module-level types into runtime products before execution. - Components remain the owners of state and the invariant-preserving operations over that state. - A component implements execution for each instruction it supports. - A composite explicitly selects the instructions that are part of its public instruction set. @@ -40,7 +42,8 @@ The resulting ownership model is: | Decision | Owner | |---|---| | What syntax is accepted from SST? | Surface instruction types and their patterns | -| How are labels, symbols, and sugar lowered? | The implementer of `Resolve` | +| What values and types exist? | The selected author-defined data model | +| How are types, labels, symbols, and sugar lowered? | The implementer of `Resolve` | | What fully resolved data is stored for execution? | Runtime instruction types | | Which component knows how to execute it? | The selected component's `Execute` implementation | | Is the instruction available in this machine? | The composite | @@ -77,8 +80,12 @@ The architecture is intended to preserve the following properties: 12. Surface instruction products use the checked pattern parser generator. 13. Runtime instruction products never contain unresolved labels or other source-only data. 14. A composite parser is constructed from only the selected surface instructions. -15. `Resolve` is the explicit, type-checked bridge from parsed surface - modules to runtime modules. +15. `Resolve` is the explicit, type-checked bridge from + parsed surface modules to runtime modules. +16. Components exchange identical author-defined boundary types or use an explicit conversion. + +The ownership and staging of value and type products are defined in +[`types-and-values.md`](./types-and-values.md). ## Non-Goals @@ -113,8 +120,8 @@ pub struct SurfaceConditionalBranch { // Runtime instruction: stored in the program image and executed. pub struct ConditionalBranch { - pub when_true: usize, - pub when_false: usize, + pub when_true: InstructionIndex, + pub when_false: InstructionIndex, } ``` @@ -158,7 +165,7 @@ pub enum MyMachineInstruction { ``` The surface sum is parsed from SST. An implementation of -`Resolve` produces a +`Resolve` produces a `Module` containing runtime instructions for the program image. The runtime sum is dispatched during execution. @@ -285,13 +292,15 @@ composite may route the same instruction type to two fields: ```rust pub enum MachineInstruction { - PushOperand(stack::Push), - PushCall(stack::Push), + PushOperand(stack::Push), + PushCall(stack::Push), } ``` -Both variants contain the same instruction type and may target the same `Stack` component -type, but they target different instances and may have different message and effect policies. +Here and below, `MachineValue` is an illustrative author-defined carrier rather than a vihaco core +type. Both variants contain the same instruction type and may target the same +`Stack` component type, but they target different instances and may have different +message and effect policies. The outer variant is therefore part of the route identity. The composite uses it to determine: @@ -330,12 +339,12 @@ A runtime instruction contains the resolved information required by execution: ```rust pub struct Branch { - pub target: usize, + pub target: InstructionIndex, } pub struct Call { pub arity: u32, - pub target: usize, + pub target: InstructionIndex, } ``` @@ -344,7 +353,7 @@ Surface instruction types therefore: - Derive `vihaco_parser::Parse`. - Own their pattern and dialect head. - May contain labels, symbolic names, literals, and other source-level values. -- Are inputs to `Resolve`. +- Are inputs to `Resolve`. - Are never executed by components. - Are not stored in the runtime program image. @@ -378,6 +387,13 @@ Runtime instructions may contain resolved semantic configuration such as: Information that depends on live machine state belongs in the runtime message rather than either instruction representation. +The types of those fields come from the instruction or data-model author. A runtime instruction +may contain `i64`, `ChannelId`, an author-defined runtime type descriptor, or another resolved +product; it does not depend on a framework `Value` or `Type` enum. Surface products similarly use +the author's value and type parsers. Module-level function signatures receive their surface type +as a separate parsed-module parameter, as described in +[`types-and-values.md`](./types-and-values.md). + ## Shape of a Component A component owns one coherent domain of state and the operations that preserve that domain's @@ -445,11 +461,11 @@ For example: ```rust pub struct MyMachine { - operand_stack: Stack, - call_stack: Stack, + operand_stack: Stack, + call_stack: Stack, arithmetic: ArithmeticUnit, - heap: Heap, - channels: Channels, + heap: Heap, + channels: Channels, program: Executor, clock: ChildClock, } @@ -461,11 +477,11 @@ composite declares its accepted surface instructions and executable runtime rout ```rust machine! { composite MyMachine { - operand_stack: Stack, - call_stack: Stack, + operand_stack: Stack, + call_stack: Stack, arithmetic: ArithmeticUnit, - heap: Heap, - channels: Channels, + heap: Heap, + channels: Channels, program: Executor, clock: ChildClock, } @@ -551,4 +567,3 @@ chosen to make public. The parent may: - Treat the child as a resource or effect handler without exposing its runtime instructions. Containment never implies recursive instruction inheritance. - diff --git a/vision/macro-generation.md b/vision/macro-generation.md index 758ebf40..892db073 100644 --- a/vision/macro-generation.md +++ b/vision/macro-generation.md @@ -12,6 +12,8 @@ Surface and runtime declarations carry different responsibilities: |---|---| | Surface instruction | Pattern, dialect head, parsed source fields | | Runtime instruction | Fully resolved fields and execution identity | +| Author surface value/type | Pattern and parsed source fields | +| Author runtime value/type | Resolved semantics and, when persisted, its byte codec | For example: @@ -25,13 +27,18 @@ pub struct SurfaceBranch { #[derive(Instruction)] pub struct Branch { - pub target: usize, + pub target: InstructionIndex, } ``` The runtime instruction derive does not introduce source syntax. Parsing remains exclusively a surface-instruction concern. +Deriving `Parse` for a value or type product does not make it part of a vihaco-wide data model. The +author selects that product in instruction fields or as the parsed module's surface type. Scalar +field parsers come from parser core. Surface instruction derives may generate the +surface-instruction marker, but do not generate runtime opcode or bytecode implementations. + ## Component Declaration The component macro: @@ -54,9 +61,10 @@ The composite/machine macro: - Verifies that each target field implements `Execute`. - Generates the surface instruction sum and its pattern parser. - Requires every selected surface instruction to implement `vihaco_parser_core::Parse<'src>`. +- Uses the author-selected module surface type for function signatures and declarations. - Generates the runtime instruction sum. -- Requires a `Resolve` implementation whose output module uses - the machine runtime instruction sum. +- Requires a `Resolve` implementation whose + output module uses the machine runtime instruction sum and author-defined constant/type products. - Generates the outer execution match. - Generates or calls route-specific message resolvers. - Generates or calls route-specific effect handlers. @@ -77,3 +85,22 @@ Effect wiring supports: Wiring remains type-checked. Macro input may contain strings for field names or source aliases, but generated execution never performs string-based runtime routing. +Wiring also never inserts a value conversion. The producer and handler types must match, or the +route must name an author-defined converter or handler with explicit semantics. + +## Bytecode Generation + +Future bytecode derives and composite generation: + +- Implement portable primitive codecs in vihaco core. +- Compose codecs implemented by the authors of runtime instruction, constant, and type products. +- Encode each component or composite's local section with its own header, payload, and data model. +- Assign explicit stable section-local route opcodes to each generated machine runtime sum. +- Recursively forward child section encoding and loading through named loadable fields. +- Preserve the file-wide global context and parent-relative child section table. +- Never derive persistent identifiers from Rust variant order or layout. +- Keep bytecode traits off surface instruction, value, and type products unless an author + independently chooses to persist one. + +The complete codec ownership model is defined in +[`types-and-values.md`](./types-and-values.md). diff --git a/vision/sst-resolution.md b/vision/sst-resolution.md index 708fcb3b..9f8e7680 100644 --- a/vision/sst-resolution.md +++ b/vision/sst-resolution.md @@ -45,6 +45,10 @@ pattern-derived parser: A missing language construct is addressed by extending the pattern generator, not by introducing a second parser or compatibility attribute system. +`#[syntax_class(instruction, ...)]` identifies a surface instruction and may generate the +framework's surface-instruction marker. That marker is independent of the runtime bytecode +`Instruction` contract: parsing a source product must not require an opcode, byte width, or decoder. + ## Value and Type Operands The `value` and `type` syntax classes let instruction fields delegate grammar to domain types: @@ -70,6 +74,21 @@ values declare their pattern explicitly. The instruction pattern consequently remains structural: it binds fields, while each field type owns its grammar. +These products are author-defined. Vihaco does not supply a semantic `SurfaceValue`, +`SurfaceType`, runtime `Value`, or runtime `Type` that every machine must use. Parser core instead +provides fallible scalar parsers and distinct lexical helpers that authors compose into their own +products. Identifiers, symbols, quoted strings, and unresolved literal text remain different +shapes rather than aliases for one catch-all `String`. + +A parsed function's parameter and return types likewise use an author-selected surface type: + +```text +ParsedModule +``` + +The complete ownership and runtime relationship is defined in +[`types-and-values.md`](./types-and-values.md). + ## Canonical Syntax Ownership A reusable surface instruction owns its canonical dialect head and pattern. The composite decides @@ -108,14 +127,17 @@ algorithm. ## Parsing Versus Resolution Pattern parsing and module resolution are consecutive but distinct boundaries. Parsing always -constructs a surface instruction. `Resolve` then uses module-wide -context to construct runtime instructions: +constructs a surface instruction and author-defined module type products. +`Resolve` then uses module-wide context to construct +runtime instructions, constants, and runtime type metadata: - Labels and symbolic branch targets require symbol resolution. - Interned strings require a module interner. - Sugar may expand one surface instruction into several runtime instructions. - Overloaded forms can be separate surface instruction types. - Machine-specific validation may depend on headers or other section metadata. +- Surface literals require author-defined range and invariant checks. +- Source-language coercions must lower to explicit conversions. The full distinction is: @@ -124,17 +146,17 @@ pattern parsing: source text -> surface instruction module resolution: - ParsedModule - -> Resolve - -> Module + ParsedModule + -> Resolve + -> Module runtime message resolution: runtime instruction + machine state -> Execute::Message ``` For `ConditionalBranch`, parsing preserves `@then` and `@otherwise` as source names. Module -resolution replaces them with `usize` program indices. Runtime message resolution may later obtain -the condition from the operand stack, but never resolves the labels again. +resolution replaces them with fixed-width `InstructionIndex` values. Runtime message resolution +may later obtain the condition from the operand stack, but never resolves the labels again. ## Naming the Three Instruction Concepts @@ -149,7 +171,6 @@ A consistent naming direction is: - `SurfaceInstruction` for the types constructed by the pattern parser. - `Instruction` for an individual runtime operation. - `MachineInstruction` or `InstructionSet` for the generated runtime sum. -- `Resolve` for module lowering. +- `Resolve` for module lowering. The exact identifiers remain an API decision; the three roles must remain visible. - diff --git a/vision/stack-machine-policy.md b/vision/stack-machine-policy.md index 5da0e3b2..789684ec 100644 --- a/vision/stack-machine-policy.md +++ b/vision/stack-machine-policy.md @@ -7,6 +7,12 @@ elsewhere cross the composite through messages and effects: > Native stack operations mutate their selected stack directly. Operations owned by another domain > obtain stack inputs through message resolution and return stack outputs through effect handling. +`V` is selected by the machine author. It may be a scalar, library newtype, or author-defined +heterogeneous carrier; the stack does not depend on a vihaco `Value` enum. The same policy applies +to frame storage, heaps, and channels. Compatible routes share `V` or another exact Rust boundary +type, while conversions use explicit instructions or handlers as defined in +[`types-and-values.md`](./types-and-values.md). + ## Native Stack Instructions Operations whose semantics are entirely stack-local naturally target the stack component: @@ -46,6 +52,11 @@ Both forms may coexist under distinct names or modules. Their difference is arch than ergonomic: one isolates arithmetic semantics, while the other owns an entire stack transition. +For a heterogeneous `Stack`, runtime message resolution may extract and validate +typed operands such as `Operands` before calling the arithmetic component. Effect handling +then wraps `ValueResult` back into the author-defined carrier. The reusable arithmetic +component need not know the stack representation. + ## Locals and Loads Separate local and operand storage uses the staged path: @@ -115,4 +126,3 @@ program-counter placement determines their destination: This makes control-flow instructions reusable across different program, cursor, frame, and driver representations without allowing both the machine and driver to advance the same cursor. - diff --git a/vision/types-and-values.md b/vision/types-and-values.md new file mode 100644 index 00000000..c75a6595 --- /dev/null +++ b/vision/types-and-values.md @@ -0,0 +1,657 @@ +# Author-Defined Types and Values + +## Status and Direction + +Vihaco does not define one built-in guest `Value` enum or `Type` enum. It provides the framework +boundaries through which a machine author supplies values and types: + +- Scalar `Parse` implementations and syntax helpers for constructing typed surface products. +- Generic parsed-module and resolved-module containers. +- Generic component, message, effect, and instruction relationships. +- Primitive byte-encoding implementations and composition of author-defined codecs. + +The author decides whether a machine needs: + +- One scalar value type such as `i64`. +- Several unrelated typed domains. +- A heterogeneous value carrier for a dynamically typed stack. +- Resolved type descriptors for signatures and typed instructions. +- No runtime type descriptor at all because Rust types carry every required distinction. + +These choices form an author-defined **data model**. A data model commonly lives in a module or +reusable crate beside the components and composites that use it. A composite commits to concrete +data-model types through its fields and routes, but containment does not make the composite the +semantic owner of those types. + +The model has the following properties: + +1. Vihaco's scalar parsers are building blocks, not a guest value system. +2. Surface values and types are ordinary author-defined Rust products implementing `Parse`. +3. Parsed modules contain typed surface instructions rather than untyped fallback forms. +4. Module-level parameter and return types use an author-selected surface type. +5. `Resolve` lowers author-defined surface products into author-defined runtime products. +6. Runtime messages and effects use the narrowest useful Rust types. +7. Components that exchange a value use the same boundary type or an explicit conversion. +8. No route performs an implicit cast merely because a value crosses a component boundary. +9. Bytecode serializes a resolved section tree; each section uses codecs for its owner's concrete + author-defined types. +10. Rust enum layout, `usize`, and implicit variant order never define a persistent bytecode ABI. + +## The Three Type Layers + +The word “type” refers to three different mechanisms that must remain distinct. + +### Rust Types + +Rust types establish framework relationships: + +```rust +impl Execute for ArithmeticUnit { + type Message = Operands; + type Effect = ValueResult; + type Fault = ArithmeticFault; + + // ... +} +``` + +They statically pair an instruction with its component, message, effect, and fault. A mismatched +route should fail to compile whenever the mismatch is visible at this layer. + +### Surface Types + +Surface types preserve the type syntax written in SST: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(type)] +pub enum CpuSurfaceType { + #[pattern = "`bool`"] + Bool, + + #[pattern = "`i64`"] + I64, + + #[pattern = "`f64`"] + F64, +} +``` + +They may also contain unresolved names, type arguments, aliases, address spaces, units, or other +source-level information: + +```rust +pub enum MachineSurfaceType { + Named(QualifiedName), + Vector { + element: Box, + length: u32, + }, +} +``` + +Vihaco does not require one universal surface-type AST. An author uses the smallest product that +faithfully represents the selected SST dialect. + +### Runtime Types and Values + +Runtime data is whatever the configured machine executes with. A statically typed machine may use +Rust scalars directly and need no guest `Type` enum: + +```rust +pub struct NumericMachine { + stack: Stack, + arithmetic: ArithmeticUnit, + channel: Channel, +} +``` + +A heterogeneous stack machine may define its own carrier: + +```rust +pub enum CpuValue { + Bool(bool), + I64(i64), + F64(f64), + Function(FunctionId), + Heap(HeapRef), +} + +pub enum CpuType { + Bool, + I64, + F64, + Function, + Heap, +} +``` + +Those enums belong to the CPU data model, not to vihaco core. Other machines may reuse them, extend +them through a new author-defined carrier, or avoid them entirely. + +## Ownership + +Type and value ownership follows semantic definition and composition rather than component +containment: + +| Concern | Owner | +|---|---| +| Scalar parsing and byte encoding | Vihaco parser/core libraries | +| Meaning of a domain type such as `HeapRef` or `ChannelId` | The library defining that domain | +| Closed value carrier for a particular architecture | The data-model or machine author | +| Surface grammar for values and types | The surface product that implements `Parse` | +| Module-level surface type | The author-selected SST dialect | +| Source type checking and lowering | `Resolve` | +| Storage and invariant-preserving mutation | The component | +| Concrete types used by fields and routes | The composite declaration | +| Cross-domain conversion semantics | An explicit author-selected instruction, adapter, or handler | +| Encoding of an author-defined type | The crate defining that type | +| Encoding of the generated machine instruction sum | The composite-generated codec | + +A component may define a type when the type is part of its reusable semantic domain. A heap library, +for example, may define `HeapRef`. That does not give each heap instance a private type system. +References that cross component boundaries retain the library-defined identity and any runtime +provenance required to select a valid heap. + +A composite does not automatically merge child types into a generated universal `Value` enum. It +selects concrete component instantiations: + +```rust +pub struct Machine { + stack: Stack, + heap: Heap, + channel: Channel, +} +``` + +The same data model may be shared by several composites. Conversely, one composite may contain +multiple independent typed domains when no universal carrier is useful. + +## Scalar Building Blocks + +Vihaco parser core supplies `Parse` implementations for the scalar source forms it supports, such +as signed and unsigned integers, floating-point numbers, and booleans. Authors compose those +parsers through fields in their own value and instruction products: + +```rust +#[derive(vihaco_parser::Parse)] +#[syntax_class(value)] +pub enum CpuLiteral { + #[pattern = "`i64` `,` $0"] + I64(i64), + + #[pattern = "`f64` `,` $0"] + F64(f64), + + #[pattern = "`bool` `,` $0"] + Bool(bool), +} +``` + +Scalar parsing is fallible. Out-of-range input returns a parse error and never panics. The set of +supported scalar parsers is an SST API decision; it need not imply that every Rust scalar is a +portable bytecode operand. + +Identifiers, quoted strings, and unresolved literal text are distinct lexical products. `String` +must not ambiguously mean all three. Vihaco may provide helpers or newtypes such as: + +```rust +pub struct Identifier(pub String); +pub struct StringLiteral(pub String); +pub struct LiteralText(pub String); +``` + +`LiteralText` is useful when a neighboring surface type determines how a token is interpreted: + +```rust +pub struct SurfaceConstant { + pub ty: CpuSurfaceType, + pub literal: LiteralText, +} +``` + +It is deliberately unresolved lexical data, not a framework-owned `SurfaceValue`. + +## Parsed Module Shape + +Typed surface instructions are the only body items in a parsed module. Unknown or malformed +instructions fail at the parser boundary rather than becoming generic mnemonic/operand records. + +Module-level signatures must use an author-selected surface type: + +```rust +pub struct ParsedModule +where + I: SurfaceInstruction, +{ + pub header: H, + pub functions: Vec>, +} + +pub struct ParsedFunction +where + I: SurfaceInstruction, +{ + pub name: String, + pub params: Vec>, + pub return_ty: Option, + pub body: Vec, +} + +pub struct Param { + pub name: String, + pub ty: Ty, +} +``` + +The exact generic ordering remains an API decision. The required property is that neither function +signatures nor instruction fields depend on a vihaco-defined surface type. + +Values usually appear inside surface instruction products and therefore need no parsed-module-wide +value parameter. If SST later gains a module-level constant declaration independent of +instructions, that declaration receives its own author-selected surface value type. + +A surface instruction implements `Parse` and the surface-instruction marker. It does not implement +the runtime bytecode `Instruction` contract. The parser derive may generate the marker for +`#[syntax_class(instruction, ...)]`; no surface product should need opcodes or byte codecs merely to +participate in `ParsedModule`. + +## Resolution + +Types and values follow the same stage boundary as instructions: + +```text +SST text + -> pattern parser + -> ParsedModule + -> Resolve + -> Module + -> runtime program image +``` + +The concrete `Constant` and `RuntimeType` parameters are author-defined. Either may be a scalar, +enum, newtype, or unit when the machine does not need that category. + +This pipeline describes the contents owned by one SST section. A multi-section SST file applies it +recursively: each section is parsed and resolved by the component or composite selected for that +section path, while the file's global context supplies shared navigation and linkage data. Parent +and child sections may use different surface instructions, surface types, runtime instructions, +constants, and runtime type descriptors. + +Resolution performs every transformation requiring source or module context: + +- Resolve surface type names, aliases, and parameters. +- Validate function signatures and declarations. +- Interpret unresolved literal text. +- Check numeric ranges and other value invariants. +- Intern strings and constants. +- Resolve functions, labels, channels, and other symbolic identities. +- Select runtime instruction routes for overloaded surface forms. +- Expand sugar into runtime instructions. +- Introduce a conversion only when the selected source language defines one. +- Reject unsupported type/value combinations before execution. + +For example: + +```text +cpu::const i64, 42 + -> SurfaceConstant { ty: I64, literal: "42" } + -> resolve and range-check + -> CpuValue::I64(42) + -> PushConstant(ConstantId(7)) +``` + +An author may instead parse directly to `CpuLiteral::I64(42)`, moving the type/literal pairing to +the parser. Both are valid. The former permits type-directed literal syntax; the latter makes more +invalid combinations unrepresentable before resolution. + +The output contains no unresolved source type names, ambiguous literals, or symbolic references. +Runtime message resolution reads live machine state; it never repeats source type or literal +resolution. + +## Runtime Dataflow + +Messages and effects use the narrowest useful Rust type. A reusable arithmetic path may be: + +```text +Stack + -> Operands + -> ArithmeticUnit + -> ValueResult + -> Stack +``` + +A heterogeneous stack may resolve and validate a dynamic value before component execution: + +```text +Stack + -> resolve two CpuValue::I64 operands + -> Operands + -> ArithmeticUnit + -> ValueResult + -> handle as CpuValue::I64 + -> Stack +``` + +This preserves dynamic storage where the architecture requires it while presenting the exact +operand type to `Execute`. + +`Undefined` is not required as a universal value or type. Uninitialized storage is normally modeled +as slot state: + +```rust +pub enum Slot { + Uninitialized, + Initialized(V), +} +``` + +An author may still define `Undefined` as a real guest value when that is part of the selected +language semantics. + +## Cross-Component Compatibility and Conversion + +Moving a value between components does not imply conversion. Compatible routes share a Rust +boundary type: + +```text +Effect -> Handler +``` + +An incompatible route is rejected: + +```text +Effect -/-> Handler +``` + +The author makes conversion explicit through one of: + +- A runtime conversion instruction. +- A conversion component. +- A named route handler. +- Resolution-time conversion of a constant. +- Resolution-time insertion of an explicit runtime conversion when the source language specifies + an implicit coercion. + +Conversion semantics are named and testable. Checked, saturating, wrapping, lossy, and bitwise +reinterpretation are not one generic `cast` operation. Vihaco core does not provide a universal +`Value::cast`, and generated message/effect wiring never invents a conversion. + +Nested composites follow the same rule. A child exports concrete boundary types. A parent either +uses those types directly or selects an explicit adapter; containment does not erase the +distinction. + +## Resolved Constants and Live Values + +Program constants and live runtime values need not have the same Rust type. + +A serializable constant may contain: + +- Scalars. +- Interned string identifiers. +- Function identifiers. +- Immutable aggregate initializers. +- Library-defined static configuration. + +A live value may additionally contain: + +- Heap references tied to an allocation generation. +- Resource handles. +- Continuation identifiers. +- Device-local references. +- Other state whose meaning exists only after loading. + +Ordinary program bytecode must not serialize a live runtime handle accidentally. An author may use +one type when every runtime value is a valid constant, or separate `Constant` and `Value` types when +their invariants differ. Snapshots are a separate format with separate ownership and validation. + +## Bytecode Encoding + +Bytecode is a serialization of a resolved multi-section program tree for a concrete machine +topology. It is not a serialization of parsed surface syntax or arbitrary Rust memory. + +The file container and a section payload have different ownership: + +- The file container owns magic, format version, flags, one global context, and the root of a + recursive section tree. +- The global context owns information intentionally shared across sections, including the mapping + used to resolve child-section name indices. +- Each section owns one local header, one local bytecode payload, one child table, and its nested + child sections. +- The component or composite selected by the section path owns the schema of that section's header + and payload. +- A generated composite loads its own section and forwards named direct child sections to the + corresponding loadable fields. + +The conceptual container shape is: + +```text +file header + magic + format version + flags + global-context length + +global context + section-name table + optional author-defined global linkage data + +root section + section frame + total section length + local header length + local author-defined header + local payload length + local author-defined payload + local module data + local instruction stream + child table + local child-name index + child offset relative to this section + encoded child sections + recursively use the same section framing +``` + +The fixed container parses framing and builds section views without interpreting local headers or +payloads. It resolves child names through the global context and validates the recursive ranges. +The selected loader then decodes each section through the concrete types of its target component or +composite. + +### Section-Local Data Models + +A bytecode file does not imply one `Value`, `Type`, constant, or instruction codec for the entire +section tree. Each section may resolve to a different local module: + +```text +root: + Module + +root/cpu_a: + Module + +root/radio: + Module +``` + +The root composite may own a local program in addition to its children, or its local payload may be +empty. Two sibling sections may reuse the same data-model crate and codec, but sharing is explicit +rather than imposed by the file. + +Runtime messages and effects may move values between the loaded components. That typed runtime +dataflow does not require their program sections to use one byte-level value representation. If a +source-level reference crosses sections, resolution represents its scope explicitly—for example +with a `SectionPath` plus a section-local identifier, or with an intentionally global identifier +allocated by the global context. + +Identifiers state their scope: + +```rust +pub struct GlobalStringId(pub u32); +pub struct SectionConstantId(pub u32); +pub struct InstructionIndex(pub u32); +``` + +An unqualified integer must not be interpreted sometimes as a global index and sometimes as a +section-local index. + +Vihaco supplies encoding and decoding contracts and implementations for portable primitives. +Libraries and authors implement or derive them for their own products: + +```rust +pub trait Encode { + fn encode(&self, output: &mut W) -> eyre::Result<()>; +} + +pub trait Decode: Sized { + fn decode(input: &mut R) -> eyre::Result; +} +``` + +The exact trait names remain an API decision. Encoding is independent from `Parse`: a surface type +may parse without being encodable, and a runtime type may be encodable without having SST syntax. + +The ownership chain is: + +| Encoded product | Codec owner | +|---|---| +| File and recursive section framing | Vihaco core | +| Global context contents | The selected global-context author | +| Fixed-width scalar | Vihaco core | +| Library newtype such as `ChannelId` | Defining library | +| Section-local value/type product | That section's data-model author | +| Section-local runtime instruction product | That section's instruction author | +| Section-local machine instruction sum and route opcode | The owning composite's generation | +| Section-local header and module metadata | The section owner | + +Encoding follows the section tree: + +```text +resolve global context + -> resolve root section with its selected resolver + -> recursively resolve each admitted child section + -> encode the global context once + -> encode each section's local header and payload with its owner + -> write each parent child table and relative offsets + -> finish recursive section lengths +``` + +An author-defined value enum may encode its own discriminant and payload. A machine using only +`i64` constants may need no value tag because the containing schema already determines the payload +type. Vihaco does not require a universal type table or value-tag registry. A self-describing type +table can be added by a data model or later tooling requirement without becoming the semantic +owner of the types. + +### Compatibility Identity + +The file format version identifies the framing contract, not every local instruction and data-model +schema. One file-wide machine ABI fingerprint is insufficient when nested reusable sections may +evolve independently. + +Compatibility can be established at the scopes where schemas are selected: + +- The file header identifies the container format. +- The selected global context identifies or validates its own schema when necessary. +- A section payload may carry a section-local schema identity or fingerprint. +- A generated loader may derive the expected section schema from the concrete field selected by the + section path. +- An optional root topology fingerprint may validate the expected section tree, but does not + replace section-local validation. + +The initial implementation may rely on the statically selected loader for a section's expected +schema. If persistent compatibility across independently versioned component libraries is a goal, +section-local identities should be added to the fixed section envelope rather than hidden in +author payload bytes. + +Route opcode values are section-local. The same numeric opcode may identify different runtime +instructions in two sections because the section path selects different decoders. Stable opcode +assignment is required within each section ABI; no file-wide opcode registry is required. + +The portable wire rules are: + +- Fixed-width integers and identifiers use explicitly selected widths and endianness. +- Persistent fields never use `usize` or Rust enum discriminants. +- Semantic identifiers use newtypes that state whether their scope is global or section-local. +- Booleans have one canonical encoding and reject other values. +- Floating-point values encode their IEEE bit representation under a documented NaN policy. +- Route opcodes are stable section ABI data, not derived implicitly from variant order. +- Variable-sized records carry checked lengths. +- Decoders reject truncated input, trailing payload bytes, invalid tags, invalid indices, and + configured resource-limit violations. +- Section lengths, header lengths, local payload lengths, child counts, and relative child offsets + use checked arithmetic. +- Direct child names are unique within their parent, expected by the selected composite, and + resolved through the global context. +- Child ranges remain inside their parent, begin after the parent data and child table, and do not + overlap. +- Branch targets refer to instruction indices within their local section unless an architecture + explicitly chooses another scope. + +The loader verifies container framing before exposing the root section view. Recursive composite +loading then validates each section path, local header, local payload schema, child set, and +section-local runtime invariants before constructing executable runtime instructions. Decoding +untrusted bytecode establishes the same per-section invariants that recursive SST resolution +establishes. + +## Consequences for the Current Rewrite + +The rewrite removes vihaco's built-in runtime `Value` and `Type` enums. It retains or introduces: + +1. Fallible primitive `Parse` implementations for the supported SST scalars. +2. Distinct helpers for identifiers, quoted strings, symbols, and unresolved literal text. +3. An author-selected surface type parameter on parsed functions and modules. +4. Typed surface instruction bodies with no generic raw-form fallback. +5. A surface-instruction marker independent of the runtime bytecode instruction trait. +6. Generic resolved modules over runtime instruction, constant, type, and extra metadata. +7. Portable scalar byte codecs and author-defined section-local codecs. +8. Recursive SST resolution and bytecode loading through the selected section owners. + +Framework-level placeholders named `SurfaceValue` or `SurfaceType` should not become semantic data +models. A lexical helper may remain under a name that states what it preserves, while a +module-level surface type becomes generic. + +Component migrations replace references to vihaco's old enums with: + +- A scalar or newtype when the component has one typed domain. +- A generic parameter when the component is reusable across data models. +- An author-defined dynamic carrier when the architecture requires heterogeneous storage. +- Explicit conversion routes where component boundary types differ. + +## Verification + +The type and value architecture is established when: + +- Vihaco exports no required guest `Value` or `Type` enum. +- A machine using only `i64` can parse, resolve, execute, and encode without defining a value enum. +- Another machine can define and use its own heterogeneous value and type enums. +- Two composites can share a data-model crate without copying its definitions. +- A parsed function signature uses the author's surface type. +- Surface instruction types do not implement runtime bytecode traits. +- Unsupported or out-of-range scalar input produces a parse error without panicking. +- Resolution rejects invalid type/literal and instruction/type combinations. +- Cross-component wiring accepts identical boundary types and rejects incompatible ones. +- Every cross-type conversion has explicit author-selected semantics. +- Constants and runtime-only handles cannot be confused accidentally. +- Bytecode round trips a root section and heterogeneous nested child sections. +- Each section may use different author-defined instructions, constants, types, and headers. +- Route opcodes and section-local identifiers are interpreted only in their owning section. +- Global and section-local identifiers cannot be confused accidentally. +- Bytecode compatibility does not depend on Rust variant order, layout, or pointer width. +- Recursive SST and bytecode loading establish equivalent per-section runtime invariants. + +## Deferred Questions + +The first implementation does not need to decide: + +- Whether a common data-model trait usefully packages an author's value and type families. +- Whether generated composite declarations should provide shorthand for repeated data-model + parameters. +- Whether a future generic tooling format needs self-describing type schemas. +- Whether section-local schema identities belong in fixed section framing or author headers. +- Whether a root topology fingerprint is useful in addition to section-local compatibility checks. +- Whether arbitrary-precision numeric literal helpers belong in vihaco parser core. +- Whether snapshots share any encoding traits with ordinary program bytecode. +- Whether runtime specialization eventually removes dynamic value checks from selected routes. + +These questions may improve ergonomics or portability. They do not change the ownership rule: +Vihaco supplies composition and scalar infrastructure; authors define the semantic types and values +their machines use. From 3876fc265b3c993ec63862ae29b14529780c05a0 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Mon, 3 Aug 2026 16:04:14 -0400 Subject: [PATCH 03/15] Added demo, vihaco concept explainer, and demo documents --- .vscode/launch.json | 14 + .vscode/tasks.json | 11 + Cargo.lock | 12 + Cargo.toml | 1 + crates/vihaco-cpu/src/instruction.rs | 1 + demos/Cargo.toml | 17 + demos/examples/demo-vihaco-concepts.md | 711 ++++++++++++++++++++ demos/examples/demo.md | 252 +++++++ demos/examples/demo.rs | 117 ++++ demos/examples/demo/src/cpu.rs | 395 +++++++++++ demos/examples/demo/src/driver.rs | 30 + demos/examples/demo/src/machine.rs | 226 +++++++ demos/examples/demo/src/surface.rs | 51 ++ demos/examples/demo/stdlib/arithmetic.rs | 130 ++++ demos/examples/demo/stdlib/channel.rs | 307 +++++++++ demos/examples/demo/stdlib/clock.rs | 229 +++++++ demos/examples/demo/stdlib/debug_trace.rs | 30 + demos/examples/demo/stdlib/stack.rs | 45 ++ demos/examples/demo/vihaco/execute.rs | 36 + demos/examples/demo/vihaco/handle.rs | 25 + demos/examples/demo/vihaco/machine_macro.rs | 12 + demos/examples/demo/vihaco/resume.rs | 10 + demos/examples/demo/vihaco/route.rs | 28 + demos/examples/demo/vihaco/supply.rs | 9 + demos/src/main.rs | 403 +++++++++++ vision/clock.md | 327 ++++----- vision/contents.md | 49 +- vision/demo.md | 467 ------------- vision/design-tradeoffs.md | 163 ----- vision/execution-pipeline.md | 126 +++- vision/implementation-plan.md | 314 --------- vision/instruction-model.md | 569 ---------------- vision/macro-generation.md | 163 ++++- vision/runtime-drivers.md | 372 ---------- vision/sst-resolution.md | 7 +- vision/stack-machine-policy.md | 13 +- vision/traits.md | 56 -- vision/vision.md | 240 ------- 38 files changed, 3583 insertions(+), 2385 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 demos/Cargo.toml create mode 100644 demos/examples/demo-vihaco-concepts.md create mode 100644 demos/examples/demo.md create mode 100644 demos/examples/demo.rs create mode 100644 demos/examples/demo/src/cpu.rs create mode 100644 demos/examples/demo/src/driver.rs create mode 100644 demos/examples/demo/src/machine.rs create mode 100644 demos/examples/demo/src/surface.rs create mode 100644 demos/examples/demo/stdlib/arithmetic.rs create mode 100644 demos/examples/demo/stdlib/channel.rs create mode 100644 demos/examples/demo/stdlib/clock.rs create mode 100644 demos/examples/demo/stdlib/debug_trace.rs create mode 100644 demos/examples/demo/stdlib/stack.rs create mode 100644 demos/examples/demo/vihaco/execute.rs create mode 100644 demos/examples/demo/vihaco/handle.rs create mode 100644 demos/examples/demo/vihaco/machine_macro.rs create mode 100644 demos/examples/demo/vihaco/resume.rs create mode 100644 demos/examples/demo/vihaco/route.rs create mode 100644 demos/examples/demo/vihaco/supply.rs create mode 100644 demos/src/main.rs delete mode 100644 vision/demo.md delete mode 100644 vision/design-tradeoffs.md delete mode 100644 vision/implementation-plan.md delete mode 100644 vision/instruction-model.md delete mode 100644 vision/runtime-drivers.md delete mode 100644 vision/traits.md delete mode 100644 vision/vision.md diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..6a524b55 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug vihaco demo", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/target/debug/examples/demo", + "cwd": "${workspaceFolder}", + "preLaunchTask": "build demo", + "sourceLanguages": ["rust"] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..ebfb6fa6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build demo", + "type": "shell", + "command": "cargo build --package vihaco-demos --example demo", + "problemMatcher": ["$rustc"] + } + ] +} diff --git a/Cargo.lock b/Cargo.lock index f02fe6b0..ce536f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -680,6 +680,18 @@ dependencies = [ "vihaco-parser-derive", ] +[[package]] +name = "vihaco-demos" +version = "0.2.0" +dependencies = [ + "chumsky", + "eyre", + "vihaco", + "vihaco-cpu", + "vihaco-parser", + "vihaco-parser-derive", +] + [[package]] name = "vihaco-doctests" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index c83eeef5..4be016e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "crates/vihaco-runtime-derive", "crates/vihaco-stdlib", "crates/vihaco-syntax", + "demos", ] [workspace.package] diff --git a/crates/vihaco-cpu/src/instruction.rs b/crates/vihaco-cpu/src/instruction.rs index 0107af8d..e470775a 100644 --- a/crates/vihaco-cpu/src/instruction.rs +++ b/crates/vihaco-cpu/src/instruction.rs @@ -480,6 +480,7 @@ mod parse_tests { .parse("cpu::const str, \"unterminated") .has_errors() ); + assert!(SurfaceInstruction::parser().parse("br @body").has_errors()); } #[test] diff --git a/demos/Cargo.toml b/demos/Cargo.toml new file mode 100644 index 00000000..ed0eaa21 --- /dev/null +++ b/demos/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "vihaco-demos" +edition = "2024" +description = "Executable demonstrations of vihaco machine composition." +version.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish = false + +[dependencies] +chumsky = "0.10" +eyre = "0.6.12" +vihaco = { workspace = true } +vihaco-cpu = { workspace = true } +vihaco-parser = { workspace = true } +vihaco-parser-derive = { workspace = true } diff --git a/demos/examples/demo-vihaco-concepts.md b/demos/examples/demo-vihaco-concepts.md new file mode 100644 index 00000000..2b5befff --- /dev/null +++ b/demos/examples/demo-vihaco-concepts.md @@ -0,0 +1,711 @@ +# Concepts in the Demo's `vihaco` Layer + +The files under [`demo/vihaco`](./demo/vihaco) contain the small +contracts used to express an instruction pipeline. They are not a complete framework API. They are +a concrete sketch of the relationships the eventual framework and its macros need to generate. + +This document explains each concept independently. The examples use deliberately small domains +such as a counter, a mailbox, and a door; they are not taken from the demo machine. + +## Concept status against current vihaco + +The contracts described here are a design sketch, not a claim that every concept is already part +of vihaco core. The following map shows how they relate to the current implementation: + +| Concept document | Current vihaco equivalent | Comparison | +|---|---|---| +| `Effects` | [`effect.rs`](../../crates/vihaco/src/effect.rs) | Already exists closely. Current `Effects` supports `None`, `One`, `Many`, mapping, flattening, and iteration. | +| `NoMessage` | [`runtime/marker.rs`](../../crates/vihaco/src/runtime/marker.rs) | The demo uses a named `NoMessage` type. Current vihaco has a general `Message` marker trait and implements it for `()`, but does not provide the same named convention. | +| `Execution` | [`vihaco-cpu/src/outcome.rs`](../../crates/vihaco-cpu/src/outcome.rs) | Current `StepOutcome` models CPU outcomes such as `Continue`, `Breakpoint`, `Halt`, and `Return`. It is broader and different from the demo's `Complete`/`Parked` suspension state. | +| `StepResult` | [`runtime/generated.rs`](../../crates/vihaco/src/runtime/generated.rs) | The demo groups effects and execution state in `StepResult`. Current generated components return `Result>`; execution state is not paired with effects in one core type. | +| `Execute` | [`#[component]`](../../crates/vihaco-derive/src/attr_component.rs) and [`GeneratedComponent`](../../crates/vihaco/src/runtime/generated.rs) | The demo uses one `Execute` implementation per instruction type. Current vihaco uses one component-level `execute` method over an instruction type, message type, and effect type, then generates `GeneratedComponent`. | +| `Supply` | [`StackMemory`](../../crates/vihaco/src/traits/machine.rs) and component-specific APIs | The demo has a general typed message-supply capability. Current vihaco has specialized state-access traits such as `StackMemory`, but no general `Supply` trait. | +| `Absorb` | [`EffectSink`](../../crates/vihaco/src/traits/event_sink.rs) | Both describe effect destinations, but `EffectSink` emits into a sink and has no fault result. The demo's `Absorb` models a component actively consuming and applying an effect. | +| `Observe` | [`runtime::Observe`](../../crates/vihaco/src/runtime/observe.rs) | Current observation can return follow-up effects, but it has no `Route` type parameter. | +| `Handle` | No direct equivalent | Current vihaco has `EffectSink` and generated dispatch, but not a route-parameterized handler with a default `Absorb` delegation path. | +| `Route` | Generated composite/device metadata | Current [`#[composite]`](../../crates/vihaco-derive/src/attr_composite.rs) and `Machine` machinery generate device and instruction routing, but the explicit per-route marker trait in this document is not currently a public core concept. | +| `Resume` | No direct core equivalent | The demo models owned completion of a parked operation explicitly. Current runtime traits do not yet expose the same generic resume contract. | +| `component!` instruction expansion | [`#[component]`](../../crates/vihaco-derive/src/attr_component.rs) | These are different layers. Current `#[component]` adapts an implementation over one instruction enum into `GeneratedComponent`; it does not split an enum into individual instruction structs. | +| `machine!` effect fanout | [`#[composite]`](../../crates/vihaco-derive/src/attr_composite.rs), [`#[observe]`](../../crates/vihaco-derive/src/attr_observe.rs), and generated dispatch | Current macros generate machine/device structure and observation support, but the planned `effects { observe ...; to ...; with ...; }` syntax does not currently exist. | + +The status of these relationships can be summarized as: + +- **Current** — the repository already provides approximately the same concept. +- **Partial** — the repository provides a related mechanism with different ownership or type + boundaries. +- **Proposed** — the concept is demonstrated here but is not currently part of vihaco core. +- **Planned macro surface** — the concept describes intended syntax or code generation that is not + implemented yet. + +### The important `Execute` difference + +The conceptual design has individually executable instruction products: + +```rust +impl Execute for ArithmeticUnit { + type Message = BinaryOperands; + type Effect = ValueResult; + type Fault = ArithmeticFault; + + fn execute( + &mut self, + instruction: &Add, + message: BinaryOperands, + ) -> Result, ArithmeticFault> { + // execute one instruction product + } +} +``` + +Current vihaco instead uses a component-level instruction sum: + +```rust +#[component( + instruction = RuntimeInstruction, + message = CPUMessage, + effect = StepOutcome, +)] +impl CPU { + fn execute( + &mut self, + instruction: RuntimeInstruction, + message: CPUMessage, + ) -> eyre::Result> { + match (instruction, message) { + // current component-level dispatch + } + } +} +``` + +The current `#[component]` macro generates an implementation of `GeneratedComponent`: + +```rust +trait GeneratedComponent { + type Instruction; + type Message; + type Effect; + + fn execute_generated( + &mut self, + instruction: Self::Instruction, + message: Self::Message, + ) -> eyre::Result>; +} +``` + +The conceptual direction is therefore more granular than the current implementation. It aims to +move instruction matching and each instruction's message/effect relationship into separate +`Execute` implementations. + +## The pipeline at a glance + +An instruction usually crosses four boundaries: + +```text +component state --Supply--> message --Execute--> effects + execution state + | + Observe (borrow) --+ + Handle (consume) --+ +``` + +If execution cannot finish immediately, the component returns `Parked`. Later, an owned completion +is given to `Resume`, which produces another `StepResult`. The parent composite owns the sequencing +and decides what to do with the result; the instruction component owns its local invariants. + +The `Effects` type in the examples comes from the surrounding framework. It represents zero, +one, or many effects. The contracts in this directory specify how those effects are produced and +consumed, but do not define the collection itself. + +## `NoMessage`: making “no input” explicit + +`NoMessage` is a marker type for an instruction whose execution needs no value resolved from the +runtime. It is preferable to using `()` everywhere because it gives the route a named, searchable +contract and leaves room for framework-level policies around message resolution. + +For example, a `ResetDisplay` instruction can state that it has no runtime input: + +```rust +struct ResetDisplay; + +impl Execute for Display { + type Message = NoMessage; + type Effect = DisplayReset; + type Fault = DisplayFault; + + fn execute( + &mut self, + _instruction: &ResetDisplay, + _message: NoMessage, + ) -> Result, DisplayFault> { + self.clear_pixels(); + Ok(StepResult { + effects: Effects::one(DisplayReset), + execution: Execution::Complete, + }) + } +} +``` + +The important distinction is between “no message is needed” and “the message happens to be an +empty value.” A route requiring a `UserId` cannot accidentally be wired to `NoMessage`, and a +component that supplies messages can be checked against the exact instruction type. + +## `Execution`: whether the instruction finished + +`Execution` has two states: + +```rust +enum Execution { + Complete, + Parked, +} +``` + +`Complete` means the parent may advance the instruction stream. `Parked` means the current +instruction is still the active instruction and must be resumed or otherwise resolved before the +parent advances. + +This state is separate from effects. An instruction can emit an effect and still park. For +example, a `WaitForDoor` operation may emit a `WaitRegistered` fact while it waits for an external +signal: + +```text +effects: [WaitRegistered] +execution: Parked +``` + +Keeping these dimensions separate prevents a parent from inferring completion merely because an +effect was emitted. It also means an effect handler can schedule a wakeup without having to mutate +the child program counter. + +## `StepResult`: the result of starting or resuming work + +`StepResult` groups the effects produced by one execution attempt with its completion state: + +```rust +struct StepResult { + effects: Effects, + execution: Execution, +} +``` + +The same shape is returned by `Execute` and `Resume`. That is useful because the parent can run +the same observation and handling pipeline after an instruction starts and after a parked +instruction wakes. + +Consider a queue read. A successful read might return: + +```text +StepResult { + effects: [ItemRead(42)], + execution: Complete, +} +``` + +An empty queue might return: + +```text +StepResult { + effects: [ReaderParked(reader_id)], + execution: Parked, +} +``` + +The parent does not need separate “normal result” and “suspension result” plumbing. It still +processes effects, then branches on `execution`. + +## `Execute`: component-owned instruction behavior + +`Execute` says that a component can execute one particular instruction type: + +```rust +trait Execute { + type Message; + type Effect; + type Fault; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} +``` + +The instruction, message, effect, and fault are associated with this specific implementation. +That is more precise than giving a component one large enum and one universal message type. + +For a simple `AddCredit` operation: + +```rust +struct AddCredit; +struct CreditAmount(u64); +struct CreditChanged(u64); + +impl Execute for Wallet { + type Message = CreditAmount; + type Effect = CreditChanged; + type Fault = WalletFault; + + fn execute( + &mut self, + _instruction: &AddCredit, + CreditAmount(amount): CreditAmount, + ) -> Result, WalletFault> { + self.balance = self + .balance + .checked_add(amount) + .ok_or(WalletFault::Overflow)?; + Ok(StepResult { + effects: Effects::one(CreditChanged(self.balance)), + execution: Execution::Complete, + }) + } +} +``` + +This allows the same `AddCredit` instruction to be selected into multiple composites, provided +each composite supplies a compatible message and handles the declared effect. The `Wallet` owns +the balance invariant; the composite owns how the message is obtained and where the effect goes. + +## `component!`: declaring a component's instruction set + +The arithmetic library shows the shape that a future `component!` macro is meant to make concise. +`ArithmeticUnit` is a reusable component, and its instruction set consists of `add`, `sub`, and +`mul`. The source currently writes the important pieces out by hand. Its commented `isa!` sketch +shows the intended declaration: + +```rust +isa! { + #[namespace("arith")] + instruction Arithmetic { + #[pattern = "'add"] + Add, + #[pattern = "'sub"] + Sub, + #[pattern = "'mul"] + Mul, + } +} +``` + +A component-oriented macro can use that instruction set as part of a declaration such as: + +```text +component! { + ArithmeticUnit { + instructions: Arithmetic, + } +} +``` + +The macro's useful expansion is not one `Execute` implementation. It turns each +instruction-set member into an individual instruction struct and preserves an enum of those +structs for grouping, parsing, storage, or dispatch: + +```rust +struct Add; +struct Sub; +struct Mul; + +enum Arithmetic { + Add(Add), + Sub(Sub), + Mul(Mul), +} +``` + +The enum is the instruction *sum*: it answers “which arithmetic operation is this value?” The +structs are the individual instruction *products*: each one can be used as the `I` in +`Execute`: + +```rust +impl Execute for ArithmeticUnit { + type Message = BinaryOperands; + type Effect = ValueResult; + type Fault = ArithmeticFault; + + fn execute( + &mut self, + _instruction: &Add, + message: BinaryOperands, + ) -> Result, ArithmeticFault> { + Ok(StepResult { + effects: Effects::one(ValueResult(message.lhs + message.rhs)), + execution: Execution::Complete, + }) + } +} +``` + +`Sub` and `Mul` can have their own `Execute` and `Execute` implementations. They may +share `BinaryOperands` and `ValueResult`, as the arithmetic component does, or declare different +message, effect, and fault types when their semantics require it. + +This split is necessary because a single enum implementation would force execution through a +large match and one broad set of associated types. Individual structs allow the type system to +record that `Add` needs `BinaryOperands`, produces `ValueResult`, and has a particular fault +model. A composite can select only `Add` without also exposing `Sub` and `Mul`, while a parser or +runtime instruction sum can still carry all three variants when it needs a single storable value. + +The component macro therefore has two related jobs: + +1. Declare or consume the component's instruction set and generate the individual instruction + products plus their grouped enum. +2. Generate the repetitive component boundary and dispatch plumbing while leaving the actual + `Execute` behavior to the component author. + +The component owns reusable instruction behavior. A composite later decides which individual +instructions are admitted, which component instance receives each one, where messages come from, +and where effects go. This keeps instruction semantics reusable without making every component +automatically expose every operation in every machine. + +## `Supply`: resolving a runtime message + +`Supply` is a capability for obtaining a message of type `M` from component state: + +```rust +trait Supply { + type Fault; + + fn supply(&mut self) -> Result; +} +``` + +For the wallet example, a route might supply an amount from a register component: + +```rust +struct Register(u64); + +impl Supply for Register { + type Fault = RegisterFault; + + fn supply(&mut self) -> Result { + Ok(CreditAmount(self.0)) + } +} +``` + +The capability keeps message resolution outside `Execute`. `Wallet` does not need to know +whether its amount came from a register, a decoded constant, a stack, or a network adapter. A +different machine can reuse `AddCredit` with a different `Supply` implementation. + +For `NoMessage`, no supplier is needed: the framework can construct `NoMessage` directly. + +## `Absorb`: a reusable effect destination + +`Absorb` describes a component that can consume an effect: + +```rust +trait Absorb { + type Fault; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; +} +``` + +A history component can absorb wallet changes without knowing how the wallet produced them: + +```rust +struct AuditLog(Vec); + +impl Absorb for AuditLog { + type Fault = std::convert::Infallible; + + fn absorb(&mut self, CreditChanged(balance): CreditChanged) -> Result<(), Self::Fault> { + self.0.push(format!("balance is now {balance}")); + Ok(()) + } +} +``` + +This enables reuse and composition. The same `CreditChanged` can be handled by a balance display, +an audit log, or a quota checker, each with its own state and fault type. `Absorb` is intentionally +machine-agnostic: it says what a component can consume, not which instruction route selected it. + +The component author implements `Absorb` as part of the component's reusable behavior. The +composite author, or generated composite code, supplies the route-specific `Handle` wiring +that decides when and where the capability is used. This is why `Absorb` does not need to know the +route that produced the effect. + +## `Observe`: non-consuming instrumentation + +`Observe` receives a borrowed effect before the semantic handler consumes it: + +```rust +trait Observe { + type Error; + + fn observe(&mut self, effect: &Effect) -> Result<(), Self::Error>; +} +``` + +The route parameter matters because the same effect type may be produced by several routes. A +simple observer can count events without taking ownership: + +```rust +struct CreditRoute; +struct Metrics { credit_events: usize } + +impl Observe for Metrics { + type Error = std::convert::Infallible; + + fn observe(&mut self, _effect: &CreditChanged) -> Result<(), Self::Error> { + self.credit_events += 1; + Ok(()) + } +} +``` + +Observation is separate from handling for two reasons. First, logging and metrics should not +become the semantic owner of an effect. Second, multiple observers can borrow the same effect in a +deterministic order before one handler consumes it. Enabling an observer should add visibility, +not change the destination or ownership of the effect. + +## `Handle`: route-specific effect handling + +`Handle` consumes an effect for one statically identified route: + +```rust +trait Handle { + type Error; + + fn handle(&mut self, effect: Effect) -> Result<(), Self::Error>; +} +``` + +The route parameter prevents ambiguous handling when one composite selects the same effect or +instruction more than once. For example, a machine could route `MessageSent` from two different +ports to one transport type while keeping their destinations distinct: + +```rust +struct LeftPort; +struct RightPort; +struct Transport; +struct MessageSent(Vec); + +impl Handle for Transport { + type Error = TransportFault; + + fn handle(&mut self, effect: MessageSent) -> Result<(), TransportFault> { + self.send_from_left(effect.0) + } +} + +impl Handle for Transport { + type Error = TransportFault; + + fn handle(&mut self, effect: MessageSent) -> Result<(), TransportFault> { + self.send_from_right(effect.0) + } +} +``` + +Without the route marker, the two implementations would collide because Rust sees the same +`Transport` target and `MessageSent` effect. More importantly, the generated composite would lose +the identity needed to route each operation correctly. + +In the usual case, `Handle` is the route-aware adapter and `Absorb` is the reusable destination +capability. The generated or hand-written `Handle` implementation commonly delegates directly to +`Absorb`: + +```rust +impl Handle for AuditLog { + type Error = >::Fault; + + fn handle(&mut self, effect: CreditChanged) -> Result<(), Self::Error> { + self.absorb(effect) + } +} +``` + +This preserves both roles: `Absorb` says that `AuditLog` can consume this effect in +any suitable context, while `Handle` says that this particular machine +route sends its effect to that destination. `Handle` can instead contain route-specific behavior +when the default delegation is not sufficient. + +## `Route`: static identity for one selected path + +`Route` is a marker trait with associated `Effect` and `Error` types: + +```rust +trait Route { + type Effect; + type Error; +} +``` + +A route is not a runtime event and not a program-counter state. It is the compile-time identity of +one path through a composite. A generated composite might create markers like these: + +```rust +struct ReadConfig; +struct ReadSecret; + +impl Route for ReadConfig { + type Effect = ConfigRead; + type Error = MachineFault; +} + +impl Route for ReadSecret { + type Effect = SecretRead; + type Error = MachineFault; +} +``` + +Route identity allows generation to associate each path with its own message supplier, component, +effect observers, handler, timing policy, and diagnostics. It also makes it possible to route the +same instruction type to two component instances without merging their wiring. + +The associated `Effect` lets generated code name the route once and derive the effect type from +it. The associated `Error` is the containing machine's normalized error boundary: lower-level +component, supplier, observer, and handler faults can be converted into it at the route boundary. + +## `Resume`: completing a parked operation + +`Resume` handles a completion for an operation that previously returned `Parked`: + +```rust +trait Resume { + type Effect; + type Fault; + + fn resume(&mut self, completion: C) -> Result, Self::Fault>; +} +``` + +The completion must be owned. It cannot contain a borrow into the parent or into a temporary +message because the parent may process it much later. + +For a door controller: + +```rust +struct OpenDoor; +struct DoorOpened; +struct OpenCompletion { request_id: u64 }; + +impl Resume for DoorController { + type Effect = DoorOpened; + type Fault = DoorFault; + + fn resume( + &mut self, + completion: OpenCompletion, + ) -> Result, DoorFault> { + self.finish_request(completion.request_id)?; + Ok(StepResult { + effects: Effects::one(DoorOpened), + execution: Execution::Complete, + }) + } +} +``` + +The parent stores or schedules `OpenCompletion`; it does not need to understand the controller's +internal state. When resumed, the controller can emit ordinary effects and use the same handling +pipeline as a newly started instruction. + +## `machine_macro.rs`: planned effect fanout + +This file is currently a design note, not an implementation. It sketches a future `machine!` +surface for declaring effect fanout: + +```text +effects { + observe metrics, trace; + to audit_log; +} +``` + +The intended expansion is: + +```text +for each effect: + metrics.observe(&effect) + trace.observe(&effect) + audit_log.handle(effect) +``` + +The observers borrow the effect, so both can inspect it. The handler receives ownership exactly +once. `to audit_log;` selects the default behavior: the generated `Handle` implementation routes +the effect to `audit_log`, normally by calling its `Absorb` implementation. + +When the machine needs custom effect-handling behavior, the destination can eventually be +overridden with `with record_credit;`: + +```text +effects { + observe metrics, trace; + with record_credit; +} +``` + +`with record_credit;` names a handler function supplied by the machine author. The generated route +uses that function instead of the default `Handle`/`Absorb` path. For example, the machine author +could provide: + +```rust +fn record_credit( + machine: &mut AccountMachine, + effect: CreditChanged, +) -> Result<(), AccountFault> { + machine.audit.push(effect.0); + Ok(()) +} +``` + +This syntax is necessary because effect routing is repetitive but semantically important: +the generated code must preserve observer order, handler ownership, route identity, and error +conversion. + +The macro should generate wiring, not invent behavior. The author still defines the component's +`Execute` implementation, the observer logic, and the handler logic. The declaration merely makes +the selected connections visible and checks that the types fit. + +## How the concepts fit together + +Here is a complete small route for `AddCredit`: + +```text +Register::supply + -> CreditAmount + -> Wallet::execute(AddCredit, CreditAmount) + -> StepResult + -> Metrics::observe(&CreditChanged) + -> AuditLog::handle(CreditChanged) + -> Execution::Complete +``` + +The same route with a waiting instruction has a different control state but the same effect +pipeline: + +```text +Mailbox::execute(ReadNext, NoMessage) + -> StepResult + -> Execution::Parked + -> later ReadCompletion + -> Mailbox::resume(ReadCompletion) + -> StepResult + -> observers and handler + -> Execution::Complete +``` + +Together, these contracts provide the useful separation: + +- `Supply` determines where runtime input comes from. +- `Execute` owns the instruction's local state transition. +- `Effects` communicates consequences without exposing component internals. +- `Observe` adds non-owning diagnostics and instrumentation. +- `Absorb` provides the reusable effect-consuming capability, while `Handle` normally delegates to + it and adds route identity; a `machine!` `with handler;` clause can eventually override that + default. +- `Execution` tells the parent whether instruction-stream progress is allowed. +- `Resume` gives suspension a typed, owned completion path. +- `Route` keeps repeated or identical-looking paths distinct. +- `NoMessage` makes the absence of runtime input explicit. +- The planned macro makes the wiring concise while preserving those boundaries. + +That separation is what lets a single instruction behavior be reused in different machines, lets a +component retain ownership of its invariants, and lets a parent composite coordinate dataflow and +suspension without reaching into child-private state. diff --git a/demos/examples/demo.md b/demos/examples/demo.md new file mode 100644 index 00000000..68016d3f --- /dev/null +++ b/demos/examples/demo.md @@ -0,0 +1,252 @@ +# Heterogeneous Two-CPU Demo + +## Purpose + +The executable example in [`demo.rs`](./demo.rs) is the +concrete integration target for the current vision. It composes ordinary Rust types into a +clock-driven machine with two instances of the same reusable `Cpu`: + +- `CpuA` starts with a value on its stack, waits for a value from `CpuB`, then multiplies. +- `CpuB` subtracts and multiplies local operands, then sends its result to `CpuA`. +- `CpuA` runs at one local cycle per three global ticks; `CpuB` runs at one local cycle per global + tick. +- A shared in-memory `ChannelFabric` supplies the two directed channels. +- A concrete `HeterogeneousMachine` owns the root event loop and + `GlobalClock`. + +This is a working reference for the instruction, component, route, effect, suspension, and +runtime boundaries. It is intentionally implemented with explicit Rust wiring so those boundaries +remain visible while the corresponding macro surface is developed. + +## Source organization + +The example is one Cargo example file assembled with `include!`: + +```text +demos/examples/demo.rs +├── demo/vihaco/ framework contracts and route plumbing +├── demo/stdlib/ stack, arithmetic, channel, clock, and tracing components +└── demo/src/ surface resolution, reusable Cpu, root machine, and test driver +``` + +The files under `demo/vihaco/` define the small contracts used by the example: `Execute`, +`Resume`, `Supply`, `Absorb`, `Observe`, `Handle`, and `Route`. The +`machine_macro.rs` file currently documents the intended effect-fanout expansion; it does not +define a macro used by the executable. + +## Machine topology + +```text +HeterogeneousMachine +├── GlobalClock +├── HashMap +├── SharedTransport +├── Cpu A +│ ├── Stack +│ ├── ArithmeticUnit +│ ├── ChannelEndpoint> endpoint 0 +│ ├── DebugTrace +│ └── program and pc +└── Cpu B + ├── Stack + ├── ArithmeticUnit + ├── ChannelEndpoint> endpoint 1 + ├── DebugTrace + └── program and pc +``` + +`Cpu` is reusable and does not know whether it is `CpuA` or `CpuB`. The root adds that instance +identity when it maps a child `CpuEvent::RunNext` or a receive wakeup into `MachineEvent`: + +```rust +enum MachineEvent { + Step(CpuId), + Resume { + id: CpuId, + continuation: ReceiveContinuation, + value: i64, + }, +} +``` + +The root has no executable instruction section of its own. It seeds both CPUs, pops the earliest +event, dispatches it, submits any returned schedule, and drains transport wakeups. When the event +queue is empty it reports `Completed` unless a CPU remains parked, in which case it reports +`Deadlock`. + +The root also owns a `HashMap`. The reusable `Cpu` does not store +its timing ratio; the root looks up the ratio for the selected instance and passes that value into +`step_at`, `resume`, and `next_boundary_at`. This keeps timing instance-specific without making it +part of the reusable CPU's state. + +## Surface and runtime programs + +The surface model is the small `SurfaceInstruction` enum: + +```rust +enum SurfaceInstruction { + Add, + Sub, + Mul, + Send(&'static str), + Recv(&'static str), +} +``` + +`resolve_program` lowers it to `RuntimeInstruction`. Arithmetic becomes a zero-sized runtime +payload (`Add`, `Sub`, or `Mul`); channel names become `ChannelId` values: + +```text +to_b | from_a -> ChannelId(0) // A to B +to_a | from_b -> ChannelId(1) // B to A +``` + +The concrete programs are deliberately small: + +```text +CpuA: recv from_b; mul +CpuB: sub; mul; send to_a +``` + +They are constructed in `main` as: + +```rust +resolve_program(&[Recv("from_b"), Mul]); +resolve_program(&[Sub, Mul, Send("to_a")]); +``` + +There is no parser or module loader in this example yet. The surface values are authored directly, +and resolution is a direct Rust function that demonstrates the required symbolic-to-runtime +boundary. + +## Components and routes + +`Stack` is a reusable `i64` operand stack. It supplies messages by popping values and absorbs +arithmetic results by pushing them. `ArithmeticUnit` is stateless and implements `Execute`, +`Execute`, and `Execute` with `BinaryOperands -> ValueResult`: + +```text +route message: Stack supplies rhs, then lhs +component: ArithmeticUnit computes wrapping add/sub/mul +route effect: ValueResult(i64) +handler: Stack absorbs and pushes the result +observer: DebugTrace records the effect +``` + +The three arithmetic routes share the `ValueResult` effect but have distinct route markers +(`routes::IntegerAdd`, `IntegerSub`, and `IntegerMul`). The route marker disambiguates generated +message, effect, observer, handler, and fault wiring. + +`ChannelEndpoint` implements `Execute` and `Execute`: + +- `Send` receives an `i64` from the stack and immediately queues it in the shared fabric. It + produces an empty `SendEffect` and completes. +- `Recv` requires `NoMessage`. If a value is queued, it emits `ReceiveEffect::Received(value)`; + otherwise it stores an owned `ReceiveContinuation`, emits `ReceiveEffect::Parked`, and returns + `Execution::Parked`. +- `resume` consumes `ReceiveCompletion`, clears the endpoint's parked state, emits the + received value, and completes the suspended receive. + +The current `ChannelFabric` has FIFO queues and one waiter slot per channel. Its `send` operation +also moves a matching waiter into a wakeup queue. The root drains that queue and schedules the +receiver at its next local clock boundary. + +## Timing and root execution + +Every runtime instruction implements `TimedInstruction`; all five operations cost one local cycle. +The root converts local cycles using the selected CPU's `GlobalTicksPerLocalCycle` value. A CPU's +`next_boundary_at` rounds a global tick up to its next local boundary before adding the instruction +duration. + +`GlobalClock` is a reusable event queue. It orders events by `(GlobalTick, sequence)` using a +`BinaryHeap`, so equal-time events are deterministic. It owns modeled time and never calls back +into the root or fetches instructions. + +The concrete root loop is: + +```text +seed Step(A) and Step(B) at global tick 0 +while the clock has an event: + pop the earliest MachineEvent + Step: fetch an owned instruction and call Cpu::step_at + Resume: deliver an owned completion through Cpu::resume + submit the child's next RunNext schedule + drain channel wakeups into root Resume events +if the queue is empty: + parked CPU -> Deadlock + otherwise -> Completed +``` + +On a completed instruction, `Cpu` advances its program counter and schedules its next instruction +after the converted duration. A parked receive does not advance the program counter or schedule +the next instruction until its completion is delivered. + +## Actual execution trace + +`main` initializes the stacks as follows. The rightmost value is the top, so `CpuB`'s first +subtraction consumes `2` and `4`, producing `2`: + +```text +CpuA: [3] +CpuB: [10, 4, 2] +``` + +The deterministic trace asserted by `src/driver.rs` is: + +```text +global 0: CpuA recv parks on ChannelId(1) +global 0: CpuB Sub +global 1: CpuB Mul +global 2: CpuB send on ChannelId(1) +global 3: CpuA wakes, recv 20 +global 6: CpuA Mul +``` + +The value flow is: + +```text +CpuB: 4 - 2 = 2 +CpuB: 10 × 2 = 20 +CpuB sends 20 on ChannelId(1) +CpuA receives 20 +CpuA: 3 × 20 = 60 +``` + +The send at global tick 2 wakes `CpuA`, but `CpuA` can resume only at its next local boundary, +global tick 3. The resumed receive completes there; its following multiplication becomes eligible +at global tick 6 because `CpuA` uses three global ticks per local cycle. + +The example asserts `RunOutcome::Completed`, a final `CpuA` stack top of `60`, empty parked state +on both endpoints, and both CPUs finished. It also asserts that the trace has exactly the six +entries above. + +## What this demo proves + +The concrete example currently demonstrates: + +- one reusable CPU instantiated twice with distinct root identities; +- explicit selection of five runtime routes from reusable components; +- surface channel-name resolution before execution; +- typed stack message and effect boundaries; +- route-specific effect observation and handling; +- owned receive continuations and `Complete`/`Parked` step outcomes; +- child-local scheduling mapped into a root-owned event sum; +- deterministic global timing with unequal local clock ratios; +- a scalar-only machine using `i64`, with no framework-wide `Value` or `Type` enum; and +- completion and deadlock as distinct root outcomes. + +The example does not yet exercise a generated parser, module loading, bytecode encoding, a general +driver abstraction, or a macro invocation. Those remain architecture work rather than behavior +provided by this concrete demo. + +## Running and testing + +From the repository root, run the example and its tests with Cargo: + +```bash +cargo run --example demo +cargo test --example demo +``` + +The executable prints the global trace, final outcome, both stacks, and each CPU's debug records, +then checks the expected completed exchange. diff --git a/demos/examples/demo.rs b/demos/examples/demo.rs new file mode 100644 index 00000000..43d5cfab --- /dev/null +++ b/demos/examples/demo.rs @@ -0,0 +1,117 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +//! Heterogeneous two-CPU demo. This is the end-to-end integration reference from +//! `demos/examples/demo.md`. +//! +//! The implementation is split into three layers under `demo/`: framework contracts in +//! `vihaco/`, reusable components in `stdlib/`, and this demo's user-written machine in `src/`. + +#![allow(dead_code)] + +use std::collections::HashMap; +use vihaco::Effects; + +include!("demo/vihaco/execute.rs"); +include!("demo/vihaco/resume.rs"); +include!("demo/vihaco/supply.rs"); +include!("demo/vihaco/handle.rs"); +include!("demo/vihaco/machine_macro.rs"); +include!("demo/vihaco/route.rs"); +include!("demo/stdlib/debug_trace.rs"); +include!("demo/stdlib/clock.rs"); +include!("demo/stdlib/stack.rs"); +include!("demo/stdlib/arithmetic.rs"); +include!("demo/stdlib/channel.rs"); +include!("demo/src/cpu.rs"); +include!("demo/src/surface.rs"); +include!("demo/src/machine.rs"); + +fn main() -> Result<(), CpuFault> { + // The two CPU programs, authored with symbolic channel names, then resolved to runtime form. + let cpu_a_program = + resolve_program(&[SurfaceInstruction::Recv("from_b"), SurfaceInstruction::Mul]); + let cpu_b_program = resolve_program(&[ + SurfaceInstruction::Sub, + SurfaceInstruction::Mul, + SurfaceInstruction::Send("to_a"), + ]); + + // Two instances of the same reusable `Cpu`. Their local-to-global ratios are owned by the + // root machine and selected by CpuId when each child is stepped. + let fabric = std::rc::Rc::new(std::cell::RefCell::new( + ChannelFabric::::with_channels(2), + )); + let transport_a = SharedTransport::new(fabric.clone()); + let transport_b = SharedTransport::new(fabric.clone()); + + let cpu_a = Cpu { + // CpuA starts with a receive and therefore parks at global tick 0. The value sent by + // CpuB becomes the second operand for its multiplication. + operand_stack: Stack::seeded(&[3]), + alu: ArithmeticUnit::new(), + channel: ChannelEndpoint::new(EndpointId(0), transport_a), + debug: DebugTrace::default(), + program: cpu_a_program, + pc: 0, + }; + let cpu_b = Cpu { + // CpuB performs subtraction and multiplication before it reaches the send. + operand_stack: Stack::seeded(&[10, 4, 2]), + alu: ArithmeticUnit::new(), + channel: ChannelEndpoint::new(EndpointId(1), transport_b), + debug: DebugTrace::default(), + program: cpu_b_program, + pc: 0, + }; + + let mut machine = HeterogeneousMachine { + clock: GlobalClock::new(), + transport: SharedTransport::new(fabric), + ticks_per_local_cycle: HashMap::from([ + (CpuId::A, GlobalTicksPerLocalCycle::new(3)?), + (CpuId::B, GlobalTicksPerLocalCycle::new(1)?), + ]), + // CpuA has three global ticks per local tick. CpuB has one global tick per local tick. + cpu_a, + cpu_b, + execution_trace: Vec::new(), + }; + + let outcome = machine.run()?; + + println!("global trace:"); + for line in &machine.execution_trace { + println!(" {line}"); + } + println!("outcome = {outcome:?}"); + println!("CpuA stack = {:?}", machine.cpu_a.operand_stack.items); + println!("CpuB stack = {:?}", machine.cpu_b.operand_stack.items); + println!("CpuA debug = {:?}", machine.cpu_a.debug.records); + println!("CpuB debug = {:?}", machine.cpu_b.debug.records); + + // Acceptance: CpuA's receive is woken at global tick 3, the next local boundary after + // CpuB's send at global tick 2. CpuA then executes its multiply at tick 6. + assert_eq!(outcome, RunOutcome::Completed); + assert_eq!( + machine.execution_trace, + vec![ + "global 0: CpuA recv parks on ChannelId(1)", + "global 0: CpuB Sub", + "global 1: CpuB Mul", + "global 2: CpuB send on ChannelId(1)", + "global 3: CpuA wakes, recv 20", + "global 6: CpuA Mul", + ] + ); + assert_eq!(machine.cpu_a.operand_stack.top(), Some(60)); + assert!(!machine.cpu_a.channel.is_parked()); + assert!(!machine.cpu_b.channel.is_parked()); + assert!(machine.cpu_a.finished()); + assert!(machine.cpu_b.finished()); + + println!("OK: heterogeneous exchange completed with 60 on CpuA, no stale continuation"); + Ok(()) +} + +include!("demo/src/driver.rs"); diff --git a/demos/examples/demo/src/cpu.rs b/demos/examples/demo/src/cpu.rs new file mode 100644 index 00000000..823fbb29 --- /dev/null +++ b/demos/examples/demo/src/cpu.rs @@ -0,0 +1,395 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +// =========================================================================================== +// === AUTHOR: the machine declaration ======================================================= +// =========================================================================================== +// +// The reusable `Cpu` composite plus the root `HeterogeneousMachine`. The route wiring below +// (`routes`, `Route`/`Handle`/`Observe` impls, per-route resolvers, and the step match) is written +// out so the complete composite boundary is visible at the invocation site. + +// =========================================================================================== +// === GENERATED BY THE COMPOSITE MACRO ===================================================== +// =========================================================================================== +// +// This entire section is the expansion of the `Cpu` composite and its five runtime routes. It is +// written out here so the generated ownership, routing, observation, and execution boundaries are +// directly readable. + +struct Cpu { + operand_stack: Stack, + alu: ArithmeticUnit, + channel: ChannelEndpoint>, + debug: DebugTrace, + program: Vec, + pc: usize, +} + +#[derive(Debug, Clone, Copy)] +enum RuntimeInstruction { + IntegerAdd(Add), + IntegerSub(Sub), + IntegerMul(Mul), + Send(Send), + Recv(Recv), +} + +mod routes { + #[derive(Debug, Clone, Copy, Default)] + pub struct IntegerAdd; + #[derive(Debug, Clone, Copy, Default)] + pub struct IntegerSub; + #[derive(Debug, Clone, Copy, Default)] + pub struct IntegerMul; + #[derive(Debug, Clone, Copy, Default)] + pub struct Send; + #[derive(Debug, Clone, Copy, Default)] + pub struct Recv; +} + +impl Route for routes::IntegerAdd { + type Effect = ValueResult; + type Error = CpuFault; +} + +impl Route for routes::IntegerSub { + type Effect = ValueResult; + type Error = CpuFault; +} + +impl Route for routes::IntegerMul { + type Effect = ValueResult; + type Error = CpuFault; +} + +impl Handle for Cpu { + type Error = CpuFault; + + fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { + self.operand_stack.absorb(effect)?; + Ok(()) + } +} + +impl Handle for Cpu { + type Error = CpuFault; + + fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { + self.operand_stack.absorb(effect)?; + Ok(()) + } +} + +impl Handle for Cpu { + type Error = CpuFault; + + fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { + self.operand_stack.absorb(effect)?; + Ok(()) + } +} + +impl Route for routes::Send { + type Effect = SendEffect; + type Error = CpuFault; +} + +impl Handle for Cpu { + type Error = CpuFault; + + fn handle(&mut self, effect: SendEffect) -> Result<(), CpuFault> { + match effect {} + } +} + +impl Route for routes::Recv { + type Effect = ReceiveEffect; + type Error = CpuFault; +} + +impl Handle, routes::Recv> for Cpu { + type Error = CpuFault; + + fn handle(&mut self, effect: ReceiveEffect) -> Result<(), CpuFault> { + match effect { + ReceiveEffect::Received(value) => self.operand_stack.push(value), + ReceiveEffect::Parked(_) => {} + } + Ok(()) + } +} + +impl TimedInstruction for RuntimeInstruction { + fn local_cycles(&self) -> LocalCycles { + match self { + RuntimeInstruction::IntegerAdd(_) + | RuntimeInstruction::IntegerSub(_) + | RuntimeInstruction::IntegerMul(_) + | RuntimeInstruction::Send(_) + | RuntimeInstruction::Recv(_) => LocalCycles::ONE, + } + } +} + +impl Cpu { + fn fetch(&self) -> Option { + self.program.get(self.pc).copied() + } + + fn finished(&self) -> bool { + self.pc >= self.program.len() && !self.channel.is_parked() + } + + fn is_parked(&self) -> bool { + self.channel.is_parked() + } + + // Keep the conversion explicit in generated route plumbing. It anchors the intended + // machine-level fault type and gives users a direct diagnostic when a component or observer + // is missing the corresponding `From<...> for CpuFault` conversion. For routes whose error is + // already `CpuFault`, this becomes the identity `Into` conversion, so Clippy reports it as + // useless; the suppression is limited to this generated dispatch boundary because the same + // expansion must support both heterogeneous and identity conversions. + #[allow(clippy::useless_conversion)] + fn execute_generated( + &mut self, + instruction: &RuntimeInstruction, + ) -> Result { + match instruction { + RuntimeInstruction::IntegerAdd(instruction) => { + let message = self.operand_stack.supply()?; + let result = self.alu.execute(instruction, message)?; + for effect in result.effects { + >::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + >::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + RuntimeInstruction::IntegerSub(instruction) => { + let message = self.operand_stack.supply()?; + let result = self.alu.execute(instruction, message)?; + for effect in result.effects { + >::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + >::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + RuntimeInstruction::IntegerMul(instruction) => { + let message = self.operand_stack.supply()?; + let result = self.alu.execute(instruction, message)?; + for effect in result.effects { + >::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + >::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + RuntimeInstruction::Send(instruction) => { + let message = self.operand_stack.supply()?; + let result = self.channel.execute(instruction, message)?; + for effect in result.effects { + >::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + >::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + RuntimeInstruction::Recv(instruction) => { + let result = self.channel.execute(instruction, NoMessage)?; + for effect in result.effects { + , routes::Recv>>::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + , routes::Recv>>::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + } + } + + // This resume path uses the same explicit, generated error normalization as dispatch above; + // some route instantiations reduce it to an identity conversion. + #[allow(clippy::useless_conversion)] + fn resume_receive_effects( + &mut self, + continuation: ReceiveContinuation, + value: i64, + ) -> Result { + let result = self.channel.resume(ReceiveCompletion { + continuation, + value, + })?; + for effect in result.effects { + , routes::Recv>>::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + , routes::Recv>>::handle(self, effect) + .map_err(Into::::into)?; + } + Ok(result.execution) + } + + /// Finish one instruction, advance the program counter when appropriate, and return owned + /// child-local scheduling work. This is CPU-internal bookkeeping; the standard clocked + /// boundary exposes only `step_at` and `resume`. + fn complete_instruction( + &mut self, + global_tick: GlobalTick, + instruction: &RuntimeInstruction, + outcome: Execution, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result>, CpuFault> { + if outcome == Execution::Complete { + self.pc += 1; + } + + if outcome == Execution::Parked { + return Ok(None); + } + + let start = self.next_boundary_at(global_tick, ticks_per_local_cycle)?; + let duration = instruction + .local_cycles() + .checked_mul(ticks_per_local_cycle)?; + let at = start + .0 + .checked_add(duration.0) + .map(GlobalTick) + .ok_or(ClockFault::GlobalTickOverflow)?; + if self.finished() { + return Ok(None); + } + Ok(Some(Schedule { + at, + event: CpuEvent::RunNext, + })) + } +} + +// =========================================================================================== +// === END GENERATED COMPOSITE SECTION ======================================================= +// =========================================================================================== + +#[derive(Debug, Clone, Copy)] +enum CpuEvent { + RunNext, +} + +impl ClockedComponent for Cpu { + type Event = CpuEvent; + type Completion = ReceiveCompletion; + type Fault = CpuFault; + + fn step_at( + &mut self, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result>, CpuFault> { + let Some(instruction) = self.fetch() else { + return Ok(None); + }; + let execution = self.execute_generated(&instruction)?; + self.complete_instruction( + global_tick, + &instruction, + execution, + ticks_per_local_cycle, + ) + } + + fn resume( + &mut self, + completion: ReceiveCompletion, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result>, CpuFault> { + let outcome = self.resume_receive_effects(completion.continuation, completion.value)?; + let instruction = self.fetch().ok_or(CpuFault::MissingInstruction)?; + self.complete_instruction( + global_tick, + &instruction, + outcome, + ticks_per_local_cycle, + ) + } + + fn next_boundary_at( + &self, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result { + let ratio = ticks_per_local_cycle.0; + let cycles = global_tick + .0 + .checked_add(ratio - 1) + .ok_or(CpuFault::Clock(ClockFault::GlobalTickOverflow))? + / ratio; + cycles + .checked_mul(ratio) + .map(GlobalTick) + .ok_or(CpuFault::Clock(ClockFault::GlobalTickOverflow)) + } +} + +// =========================================================================================== +// === GENERATED ERROR PLUMBING =============================================================== +// =========================================================================================== +// +// The composite macro supplies the route error conversions so generated pipeline code can use +// `?` across component boundaries. + +/// Machine-level fault, with the `From` conversions generated for route plumbing. +#[derive(Debug)] +enum CpuFault { + Stack(StackFault), + Clock(ClockFault), + UnknownEndpoint, + MissingInstruction, + MissingTiming, +} + +impl From for CpuFault { + fn from(fault: StackFault) -> Self { + CpuFault::Stack(fault) + } +} + +impl From for CpuFault { + fn from(never: std::convert::Infallible) -> Self { + match never {} + } +} + +impl From for CpuFault { + fn from(fault: ClockFault) -> Self { + CpuFault::Clock(fault) + } +} + +// =========================================================================================== +// === END GENERATED ERROR PLUMBING =========================================================== +// =========================================================================================== diff --git a/demos/examples/demo/src/driver.rs b/demos/examples/demo/src/driver.rs new file mode 100644 index 00000000..c403e090 --- /dev/null +++ b/demos/examples/demo/src/driver.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instruction_timing_belongs_to_the_instruction() { + assert_eq!( + RuntimeInstruction::IntegerAdd(Add).local_cycles(), + LocalCycles::ONE + ); + assert_eq!( + RuntimeInstruction::Recv(Recv { + channel: CHANNEL_A_TO_B, + }) + .local_cycles(), + LocalCycles::ONE + ); + } + + #[test] + fn clocked_component_rejects_zero_ratio() { + assert!(matches!( + GlobalTicksPerLocalCycle::new(0), + Err(ClockFault::ZeroTickRatio) + )); + } +} diff --git a/demos/examples/demo/src/machine.rs b/demos/examples/demo/src/machine.rs new file mode 100644 index 00000000..a8ad2044 --- /dev/null +++ b/demos/examples/demo/src/machine.rs @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +// =========================================================================================== +// === AUTHOR: the top-level composite and its root event loop =============================== +// =========================================================================================== +// +// `HeterogeneousMachine` is the single top-level composite and the runtime root. It has no local +// executable instruction section and does not implement the child instruction-dispatch role; it +// owns the root event loop, interprets the machine-specific `MachineEvent` sum, and maps each +// child-local scheduling request into the appropriate root event variant. The reusable `Cpu` never +// constructs a `MachineEvent` or names itself `CpuA`/`CpuB`; parent routing attaches that identity. + +/// Which CPU an event or waiter refers to. The reusable CPU is oblivious to this tag. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum CpuId { + A, + B, +} + +/// The machine-specific owned event sum interpreted by the root. The generic `GlobalClock` is +/// parameterized over this type and never inspects it. +#[derive(Debug, Clone, Copy)] +enum MachineEvent { + /// Run the next instruction of the identified CPU. + Step(CpuId), + /// Resume a receive at the receiver's next local clock boundary. + Resume { + id: CpuId, + continuation: ReceiveContinuation, + value: i64, + }, +} + +/// How the machine terminated. +#[derive(Debug, PartialEq, Eq)] +enum RunOutcome { + /// Both programs finished with no lost value, stale continuation, or pending event. + Completed, + /// Every runnable CPU is parked and no delivery can satisfy any continuation. + Deadlock, +} + +struct HeterogeneousMachine { + clock: GlobalClock, + transport: SharedTransport, + ticks_per_local_cycle: HashMap, + cpu_a: Cpu, + cpu_b: Cpu, + /// A human-readable record of the deterministic global trace, asserted by the driver. + execution_trace: Vec, +} + +impl HeterogeneousMachine { + fn cpu_mut(&mut self, id: CpuId) -> &mut Cpu { + match id { + CpuId::A => &mut self.cpu_a, + CpuId::B => &mut self.cpu_b, + } + } + + fn cpu_ref(&self, id: CpuId) -> &Cpu { + match id { + CpuId::A => &self.cpu_a, + CpuId::B => &self.cpu_b, + } + } + + fn ticks_per_local_cycle( + &self, + id: CpuId, + ) -> Result { + self.ticks_per_local_cycle + .get(&id) + .copied() + .ok_or(CpuFault::MissingTiming) + } + + fn label(id: CpuId) -> &'static str { + match id { + CpuId::A => "CpuA", + CpuId::B => "CpuB", + } + } + + /// The root run loop. Repeatedly removes the earliest owned event, dispatches it to the + /// selected child (the borrow of `GlobalClock` ends before the child is stepped), and inserts + /// any resulting scheduling requests back into the clock. Terminates when the timeline is + /// exhausted, distinguishing normal completion from deadlock. + fn run(&mut self) -> Result { + // Seed both CPUs to run their first instruction at global tick 0. + self.clock + .schedule_at(GlobalTick::ZERO, MachineEvent::Step(CpuId::A))?; + self.clock + .schedule_at(GlobalTick::ZERO, MachineEvent::Step(CpuId::B))?; + + while let Some((tick, event)) = self.clock.pop_earliest() { + match event { + MachineEvent::Step(id) => self.step_cpu(id, tick)?, + MachineEvent::Resume { + id, + continuation, + value, + } => self.resume_receiver(id, continuation, value, tick)?, + } + self.drain_wakeups(tick)?; + } + + // The queue is empty. If any CPU is still parked, no delivery can wake it. + if self.cpu_a.is_parked() || self.cpu_b.is_parked() { + Ok(RunOutcome::Deadlock) + } else { + Ok(RunOutcome::Completed) + } + } + + /// Dispatch one instruction for `id` at global `tick`, attaching instance identity to the + /// child-local scheduling work it produces. + fn step_cpu(&mut self, id: CpuId, tick: GlobalTick) -> Result<(), CpuFault> { + // Obtain an owned instruction; the borrow of program storage ends here. + let Some(instruction) = self.cpu_ref(id).fetch() else { + // Program exhausted: the CPU simply drops out of the runnable set. + return Ok(()); + }; + + let ticks_per_local_cycle = self.ticks_per_local_cycle(id)?; + let schedule = self + .cpu_mut(id) + .step_at(tick, ticks_per_local_cycle)?; + self.submit_schedule(id, schedule)?; + + let parked = self.cpu_ref(id).is_parked(); + let detail = match instruction { + RuntimeInstruction::IntegerAdd(_) if parked => "Add parks".to_owned(), + RuntimeInstruction::IntegerSub(_) if parked => "Sub parks".to_owned(), + RuntimeInstruction::IntegerMul(_) if parked => "Mul parks".to_owned(), + RuntimeInstruction::IntegerAdd(_) => "Add".to_owned(), + RuntimeInstruction::IntegerSub(_) => "Sub".to_owned(), + RuntimeInstruction::IntegerMul(_) => "Mul".to_owned(), + RuntimeInstruction::Send(send) if parked => { + format!("send parks on {:?}", send.channel) + } + RuntimeInstruction::Send(send) => format!("send on {:?}", send.channel), + RuntimeInstruction::Recv(recv) if parked => { + format!("recv parks on {:?}", recv.channel) + } + RuntimeInstruction::Recv(recv) => format!("recv on {:?}", recv.channel), + }; + self.record(tick, id, detail); + + Ok(()) + } + + /// A delivery satisfied a parked receiver: complete its `recv` (push the value, advance past + /// the `recv`), then schedule its next instruction after its own local duration. + fn resume_receiver( + &mut self, + id: CpuId, + continuation: ReceiveContinuation, + value: i64, + tick: GlobalTick, + ) -> Result<(), CpuFault> { + let ticks_per_local_cycle = self.ticks_per_local_cycle(id)?; + // The child owns continuation completion, including its stack and parked state. The root + // only supplies the opaque continuation input and resubmits the returned schedule. + let schedule = self + .cpu_mut(id) + .resume( + ReceiveCompletion { + continuation, + value, + }, + tick, + ticks_per_local_cycle, + )?; + self.submit_schedule(id, schedule)?; + self.record(tick, id, format!("wakes, recv {value}")); + Ok(()) + } + + fn drain_wakeups(&mut self, tick: GlobalTick) -> Result<(), CpuFault> { + while let Some((continuation, value)) = self.transport.take_wakeup() { + let id = match continuation.endpoint { + EndpointId(0) => CpuId::A, + EndpointId(1) => CpuId::B, + EndpointId(_) => return Err(CpuFault::UnknownEndpoint), + }; + let ticks_per_local_cycle = self.ticks_per_local_cycle(id)?; + let at = self + .cpu_ref(id) + .next_boundary_at(tick, ticks_per_local_cycle)?; + self.clock.schedule_at( + at, + MachineEvent::Resume { + id, + continuation, + value, + }, + )?; + } + Ok(()) + } + + /// Map child-local scheduling work to a root event and submit it to the definitive global + /// clock. The child has already applied its opaque timing policy; the root only attaches `id`. + fn submit_schedule( + &mut self, + id: CpuId, + schedule: Option>, + ) -> Result<(), CpuFault> { + if let Some(Schedule { at, event }) = schedule { + match event { + CpuEvent::RunNext => self.clock.schedule_at(at, MachineEvent::Step(id))?, + } + } + Ok(()) + } + + fn record(&mut self, tick: GlobalTick, id: CpuId, detail: String) { + self.execution_trace.push(format!( + "global {:>2}: {} {detail}", + tick.0, + Self::label(id) + )); + } +} diff --git a/demos/examples/demo/src/surface.rs b/demos/examples/demo/src/surface.rs new file mode 100644 index 00000000..a37bcc5b --- /dev/null +++ b/demos/examples/demo/src/surface.rs @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +// =========================================================================================== +// === AUTHOR: surface programs and channel-name resolution ================================== +// =========================================================================================== +// +// The surface form carries symbolic channel names. The machine's resolution step turns each name +// into the library-defined `ChannelId` used by the communication component (requirement 10). + +/// A surface instruction as authored, before channel names are resolved. +#[derive(Debug, Clone, Copy)] +enum SurfaceInstruction { + Add, + Sub, + Mul, + Send(&'static str), + Recv(&'static str), +} + +/// The two directed channels wired into this machine. +const CHANNEL_A_TO_B: ChannelId = ChannelId(0); +const CHANNEL_B_TO_A: ChannelId = ChannelId(1); + +/// Resolve a symbolic channel name to its runtime identifier. `to_b`/`from_a` name the A->B +/// channel; `to_a`/`from_b` name the B->A channel. +fn resolve_channel(name: &str) -> ChannelId { + match name { + "to_b" | "from_a" => CHANNEL_A_TO_B, + "to_a" | "from_b" => CHANNEL_B_TO_A, + other => panic!("unknown channel name: {other}"), + } +} + +/// Lower a whole surface program to runtime instructions, resolving channel names along the way. +fn resolve_program(surface: &[SurfaceInstruction]) -> Vec { + surface + .iter() + .map(|instruction| match *instruction { + SurfaceInstruction::Add => RuntimeInstruction::IntegerAdd(Add), + SurfaceInstruction::Sub => RuntimeInstruction::IntegerSub(Sub), + SurfaceInstruction::Mul => RuntimeInstruction::IntegerMul(Mul), + SurfaceInstruction::Send(name) => RuntimeInstruction::Send(Send { + channel: resolve_channel(name), + }), + SurfaceInstruction::Recv(name) => RuntimeInstruction::Recv(Recv { + channel: resolve_channel(name), + }), + }) + .collect() +} diff --git a/demos/examples/demo/stdlib/arithmetic.rs b/demos/examples/demo/stdlib/arithmetic.rs new file mode 100644 index 00000000..cde02076 --- /dev/null +++ b/demos/examples/demo/stdlib/arithmetic.rs @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A reusable, stateless `i64` arithmetic component implementing `add`, `sub`, and `mul`. It is pure +/// behavior: it does not know which CPU contains it, which stack supplied the values, or how long an +/// operation takes. Because it carries no timing, the same component works unchanged whether +/// execution is driven by a clock or by real time. +struct ArithmeticUnit; + +impl ArithmeticUnit { + fn new() -> Self { + Self + } +} + +/* +component! { + component ArithmeticUnit; + + #[namespace("arith")] + instruction Arithmetic { + #[pattern = "'add"] + Add, + #[pattern = "'sub"] + Sub, + #[pattern = "'mul"] + Mul, + } +} +*/ + +enum Arithmetic { + Add(Add), + Sub(Sub), + Mul(Mul), +} + +/// The three arithmetic runtime instructions. Each is a distinct payload type so `Execute` can +/// select the operation, while the surrounding route ZST selects where the result lands. +#[derive(Debug, Clone, Copy)] +struct Add; +#[derive(Debug, Clone, Copy)] +struct Sub; +#[derive(Debug, Clone, Copy)] +struct Mul; + +/// The message the composite resolves for an arithmetic op (its two operands). +struct BinaryOperands { + lhs: i64, + rhs: i64, +} + +/// The semantic effect arithmetic produces. It carries a value and names no destination, which is +/// why several routes can share it. +#[derive(Debug)] +struct ValueResult(i64); + +impl Execute for ArithmeticUnit { + type Message = BinaryOperands; + type Effect = ValueResult; + type Fault = std::convert::Infallible; + + fn execute( + &mut self, + _instruction: &Add, + message: BinaryOperands, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(ValueResult(message.lhs.wrapping_add(message.rhs))), + execution: Execution::Complete, + }) + } +} + +impl Execute for ArithmeticUnit { + type Message = BinaryOperands; + type Effect = ValueResult; + type Fault = std::convert::Infallible; + + fn execute( + &mut self, + _instruction: &Sub, + message: BinaryOperands, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(ValueResult(message.lhs.wrapping_sub(message.rhs))), + execution: Execution::Complete, + }) + } +} + +impl Execute for ArithmeticUnit { + type Message = BinaryOperands; + type Effect = ValueResult; + type Fault = std::convert::Infallible; + + fn execute( + &mut self, + _instruction: &Mul, + message: BinaryOperands, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(ValueResult(message.lhs.wrapping_mul(message.rhs))), + execution: Execution::Complete, + }) + } +} + +/// How a stack swallows an arithmetic result, written once and reused by every `effects to ` +/// route. The macro never synthesizes handler behavior; it names a field. +impl Absorb for Stack { + type Fault = StackFault; + + fn absorb(&mut self, effect: ValueResult) -> Result<(), StackFault> { + self.push(effect.0); + Ok(()) + } +} + +/// How a stack yields a pair of operands, written once and reused by every `message from ` +/// route. Pops rhs then lhs to preserve stack order. +impl Supply for Stack { + type Fault = StackFault; + + fn supply(&mut self) -> Result { + let rhs = self.pop()?; + let lhs = self.pop()?; + Ok(BinaryOperands { lhs, rhs }) + } +} diff --git a/demos/examples/demo/stdlib/channel.rs b/demos/examples/demo/stdlib/channel.rs new file mode 100644 index 00000000..573a30db --- /dev/null +++ b/demos/examples/demo/stdlib/channel.rs @@ -0,0 +1,307 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::rc::Rc; + +/// A library-defined runtime channel identifier. Surface channel names resolve to this before +/// execution; the CPU and arithmetic components never see the symbolic name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ChannelId(usize); + +#[derive(Debug, Clone, Copy)] +struct Send { + channel: ChannelId, +} + +#[derive(Debug, Clone, Copy)] +struct Recv { + channel: ChannelId, +} + +/// Identity used by the transport to return a completion to the endpoint that parked. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct EndpointId(u8); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ReceiveContinuation { + endpoint: EndpointId, + channel: ChannelId, +} + +struct ReceiveCompletion { + continuation: ReceiveContinuation, + value: M, +} + +enum ReceivePoll { + Ready(M), + Parked(ReceiveContinuation), +} + +/// A transport is a capability supplied to a channel endpoint. It knows nothing about CPUs or +/// composite containment. Wakeups are owned by the transport and polled by the runtime root. +trait Transport { + type Fault; + + fn send(&mut self, channel: ChannelId, value: M) -> Result<(), Self::Fault>; + + fn receive( + &mut self, + endpoint: EndpointId, + channel: ChannelId, + ) -> Result, Self::Fault>; + + fn take_wakeup(&mut self) -> Option<(ReceiveContinuation, M)>; +} + +/// A reusable shared communication component. This demo uses immediate delivery; another +/// transport can implement latency or topology policy without changing `ChannelEndpoint` or `Cpu`. +struct ChannelFabric { + queues: Vec>, + waiters: Vec>, + wakeups: VecDeque<(ReceiveContinuation, M)>, +} + +impl ChannelFabric { + fn with_channels(count: usize) -> Self { + Self { + queues: (0..count).map(|_| VecDeque::new()).collect(), + waiters: vec![None; count], + wakeups: VecDeque::new(), + } + } +} + +impl Transport for ChannelFabric { + type Fault = std::convert::Infallible; + + fn send(&mut self, channel: ChannelId, value: M) -> Result<(), Self::Fault> { + match self.waiters[channel.0].take() { + Some(waiter) => self.wakeups.push_back((waiter, value)), + None => self.queues[channel.0].push_back(value), + } + + Ok(()) + } + + fn receive( + &mut self, + endpoint: EndpointId, + channel: ChannelId, + ) -> Result, Self::Fault> { + if let Some(value) = self.queues[channel.0].pop_front() { + return Ok(ReceivePoll::Ready(value)); + } + + let continuation = ReceiveContinuation { endpoint, channel }; + // TODO: this currently assumes that only one endpoint can wait on a specific + // channel at a time, so if two endpoints try to wait on the same channel, + // say A is waiting, then B tries to listen, B will replace A and A will be + // softlocked. change this to allow for multiple waiters on a single channel + debug_assert!(self.waiters[channel.0].is_none()); + self.waiters[channel.0] = Some(continuation); + Ok(ReceivePoll::Parked(continuation)) + } + + fn take_wakeup(&mut self) -> Option<(ReceiveContinuation, M)> { + self.wakeups.pop_front() + } +} + +/// The capability copied into each endpoint. Cloning it shares the transport, not endpoint state. +#[derive(Clone)] +struct SharedTransport(Rc>>); + +impl SharedTransport { + fn new(fabric: Rc>>) -> Self { + Self(fabric) + } +} + +impl Transport for SharedTransport { + type Fault = std::convert::Infallible; + + fn send(&mut self, channel: ChannelId, value: M) -> Result<(), Self::Fault> { + self.0.borrow_mut().send(channel, value) + } + + fn receive( + &mut self, + endpoint: EndpointId, + channel: ChannelId, + ) -> Result, Self::Fault> { + self.0.borrow_mut().receive(endpoint, channel) + } + + fn take_wakeup(&mut self) -> Option<(ReceiveContinuation, M)> { + self.0.borrow_mut().take_wakeup() + } +} + +#[derive(Debug)] +enum SendEffect {} + +#[derive(Debug)] +enum ReceiveEffect { + Received(M), + Parked(ReceiveContinuation), +} + +/// The component on which the communication instructions execute. Its transport is supplied at +/// construction, so its behavior is independent of where the CPU is placed in a machine. +struct ChannelEndpoint { + id: EndpointId, + transport: T, + parked: Option, + _message: PhantomData M>, +} + +impl ChannelEndpoint { + fn new(id: EndpointId, transport: T) -> Self { + Self { + id, + transport, + parked: None, + _message: PhantomData, + } + } + + fn is_parked(&self) -> bool { + self.parked.is_some() + } +} + +impl Execute for ChannelEndpoint +where + T: Transport, +{ + type Message = M; + type Effect = SendEffect; + type Fault = T::Fault; + + fn execute( + &mut self, + instruction: &Send, + value: M, + ) -> Result, Self::Fault> { + self.transport.send(instruction.channel, value)?; + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } +} + +impl Execute for ChannelEndpoint +where + T: Transport, +{ + type Message = NoMessage; + type Effect = ReceiveEffect; + type Fault = T::Fault; + + fn execute( + &mut self, + instruction: &Recv, + _message: NoMessage, + ) -> Result, Self::Fault> { + match self.transport.receive(self.id, instruction.channel)? { + ReceivePoll::Ready(value) => Ok(StepResult { + effects: Effects::one(ReceiveEffect::Received(value)), + execution: Execution::Complete, + }), + ReceivePoll::Parked(continuation) => { + self.parked = Some(continuation); + Ok(StepResult { + effects: Effects::one(ReceiveEffect::Parked(continuation)), + execution: Execution::Parked, + }) + } + } + } +} + +impl Resume> for ChannelEndpoint { + type Effect = ReceiveEffect; + type Fault = std::convert::Infallible; + + fn resume( + &mut self, + completion: ReceiveCompletion, + ) -> Result, Self::Fault> { + debug_assert_eq!(self.parked, Some(completion.continuation)); + self.parked = None; + Ok(StepResult { + effects: Effects::one(ReceiveEffect::Received(completion.value)), + execution: Execution::Complete, + }) + } +} + +#[cfg(test)] +mod channel_tests { + use super::*; + + #[test] + fn queued_values_are_fifo() { + let fabric = Rc::new(RefCell::new(ChannelFabric::with_channels(1))); + let mut endpoint = ChannelEndpoint::new(EndpointId(0), SharedTransport::new(fabric)); + + endpoint + .execute(&Send { channel: ChannelId(0) }, 10) + .unwrap(); + endpoint + .execute(&Send { channel: ChannelId(0) }, 20) + .unwrap(); + + let first = endpoint + .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .unwrap() + .effects + .into_iter() + .next(); + assert!(matches!(first, Some(ReceiveEffect::Received(10)))); + + let second = endpoint + .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .unwrap() + .effects + .into_iter() + .next(); + assert!(matches!(second, Some(ReceiveEffect::Received(20)))); + } + + #[test] + fn send_execute_wakes_a_parked_recv_execute() { + let fabric = Rc::new(RefCell::new(ChannelFabric::with_channels(1))); + let mut receiver = ChannelEndpoint::new(EndpointId(1), SharedTransport::new(fabric.clone())); + let mut sender = ChannelEndpoint::new(EndpointId(0), SharedTransport::new(fabric.clone())); + + let parked = receiver + .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .unwrap() + .effects + .into_iter() + .next(); + assert!(matches!(parked, Some(ReceiveEffect::Parked(_)))); + assert!(receiver.is_parked()); + + sender + .execute(&Send { channel: ChannelId(0) }, 42) + .unwrap(); + + let (continuation, value) = receiver.transport.take_wakeup().unwrap(); + let effects = receiver + .resume(ReceiveCompletion { continuation, value }) + .unwrap() + .effects; + assert!(matches!( + effects.into_iter().next(), + Some(ReceiveEffect::Received(42)) + )); + assert!(!receiver.is_parked()); + } +} diff --git a/demos/examples/demo/stdlib/clock.rs b/demos/examples/demo/stdlib/clock.rs new file mode 100644 index 00000000..bc14e438 --- /dev/null +++ b/demos/examples/demo/stdlib/clock.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +// =========================================================================================== +// === AUTHOR: reusable, machine-agnostic components ========================================= +// =========================================================================================== +// +// None of these know about routes, `CpuA`/`CpuB`, `MachineEvent`, or `HeterogeneousMachine`. They +// are the `stack` / `arithmetic` / `clock` / `channel` library pieces the demo composes. In this +// first demo their shared boundary type is `i64`, so no cross-component cast is ever performed. + +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +/// A generic, deterministic event queue over a machine-defined event sum `E`. +/// +/// Note on layering: this is a reusable library component, not vihaco core (see `demo.md`: +/// "local and global clocks" live under reusable component libraries, and "the reusable library +/// item is `GlobalClock`, not a general runtime driver"). What core actually owns is the +/// contract around it: the owned child-step outcome (`Execution::Complete`/`Parked`), route +/// dispatch, and the runtime root owning an event loop. The queue policy itself is swappable. A +/// machine wanting fixed or state-dependent latency drops in a different component with the same +/// shape, the way `ChannelFabric` is swappable, without changing core. +/// +/// It owns timeline state (`now`, a monotonic `seq`) but never calls back into its containing +/// composite, fetches an instruction, or knows the machine's private fields. Events are ordered by +/// `(tick, seq)`; the sequence number gives stable ordering to events scheduled for the same global +/// tick. Host execution time never contributes to modeled duration. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct GlobalTick(u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct GlobalDuration(u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct LocalCycles(u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct GlobalTicksPerLocalCycle(u64); + +/// Runtime instructions provide the local duration of their own operation. +trait TimedInstruction { + fn local_cycles(&self) -> LocalCycles; +} + +/// Owned scheduling work returned by a clocked component. The parent adds any child identity +/// before submitting the request to its root `GlobalClock`. +struct Schedule { + at: GlobalTick, + event: E, +} + +/// Generic boundary for a component that participates in a global event loop. +/// +/// The trait shares only clock vocabulary with `GlobalClock`: ticks, instruction timing, and +/// owned scheduling requests. It does not depend on a particular clock implementation or root +/// event enum. Components supply their own instruction, event, completion, and fault types. +trait ClockedComponent { + type Event; + type Completion; + type Fault; + + fn step_at( + &mut self, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result>, Self::Fault>; + fn resume( + &mut self, + completion: Self::Completion, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result>, Self::Fault>; + fn next_boundary_at( + &self, + global_tick: GlobalTick, + ticks_per_local_cycle: GlobalTicksPerLocalCycle, + ) -> Result; +} + +impl GlobalTick { + const ZERO: Self = Self(0); + + fn checked_add(self, duration: GlobalDuration) -> Result { + self.0 + .checked_add(duration.0) + .map(Self) + .ok_or(ClockFault::GlobalTickOverflow) + } +} + +impl LocalCycles { + const ONE: Self = Self(1); + + fn checked_add(self, other: Self) -> Result { + self.0 + .checked_add(other.0) + .map(Self) + .ok_or(ClockFault::LocalCycleOverflow) + } + + fn checked_mul(self, ratio: GlobalTicksPerLocalCycle) -> Result { + self.0 + .checked_mul(ratio.0) + .map(GlobalDuration) + .ok_or(ClockFault::DurationOverflow) + } +} + +impl GlobalTicksPerLocalCycle { + fn new(value: u64) -> Result { + (value != 0) + .then_some(Self(value)) + .ok_or(ClockFault::ZeroTickRatio) + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ClockFault { + ZeroTickRatio, + LocalCycleOverflow, + DurationOverflow, + GlobalTickOverflow, + SequenceOverflow, + SchedulingInPast, +} + +struct GlobalClock { + now: GlobalTick, + seq: u64, + pending: BinaryHeap>, +} + +struct Scheduled { + tick: GlobalTick, + seq: u64, + event: E, +} + +// `BinaryHeap` is a max-heap, so reverse the natural `(tick, seq)` ordering. This makes the +// earliest event the heap's greatest element while keeping the event payload unconstrained. +impl Ord for Scheduled { + fn cmp(&self, other: &Self) -> Ordering { + other + .tick + .cmp(&self.tick) + .then_with(|| other.seq.cmp(&self.seq)) + } +} + +impl PartialOrd for Scheduled { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Scheduled { + fn eq(&self, other: &Self) -> bool { + (self.tick, self.seq) == (other.tick, other.seq) + } +} + +impl Eq for Scheduled {} + +impl GlobalClock { + fn new() -> Self { + Self { + now: GlobalTick::ZERO, + seq: 0, + pending: BinaryHeap::new(), + } + } + + /// Insert owned scheduling work at an absolute global tick. + fn schedule_at(&mut self, tick: GlobalTick, event: E) -> Result<(), ClockFault> { + if tick < self.now { + return Err(ClockFault::SchedulingInPast); + } + let seq = self + .seq + .checked_add(1) + .ok_or(ClockFault::SequenceOverflow)?; + self.seq = seq; + self.pending.push(Scheduled { tick, seq, event }); + Ok(()) + } + + /// Convert child-local relative work into an absolute global tick. + fn schedule_after(&mut self, after: GlobalDuration, event: E) -> Result<(), ClockFault> { + self.schedule_at(self.now.checked_add(after)?, event) + } + + /// Remove the earliest owned event by `(tick, seq)`, advancing `now` to it. Returns the event + /// and its tick, or `None` when the timeline is exhausted. + fn pop_earliest(&mut self) -> Option<(GlobalTick, E)> { + let Scheduled { tick, event, .. } = self.pending.pop()?; + // Global time is monotonic: `now` only ever advances to the dispatched event's tick. + self.now = tick; + Some((tick, event)) + } + + fn now(&self) -> GlobalTick { + self.now + } + + fn is_empty(&self) -> bool { + self.pending.is_empty() + } +} + +#[cfg(test)] +mod clock_tests { + use super::*; + + #[test] + fn heap_returns_events_in_timeline_order() { + let mut clock = GlobalClock::new(); + clock.schedule_at(GlobalTick(10), 10).unwrap(); + clock.schedule_at(GlobalTick(2), 2).unwrap(); + clock.schedule_at(GlobalTick(2), 3).unwrap(); + clock.schedule_at(GlobalTick(1), 1).unwrap(); + + assert_eq!(clock.pop_earliest(), Some((GlobalTick(1), 1))); + assert_eq!(clock.pop_earliest(), Some((GlobalTick(2), 2))); + assert_eq!(clock.pop_earliest(), Some((GlobalTick(2), 3))); + assert_eq!(clock.pop_earliest(), Some((GlobalTick(10), 10))); + assert_eq!(clock.pop_earliest(), None); + } +} diff --git a/demos/examples/demo/stdlib/debug_trace.rs b/demos/examples/demo/stdlib/debug_trace.rs new file mode 100644 index 00000000..e1a4ea22 --- /dev/null +++ b/demos/examples/demo/stdlib/debug_trace.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A generic debug component that records every observed effect with its route's type name. +#[derive(Debug, Default)] +struct DebugTrace { + records: Vec, +} + +#[derive(Debug)] +struct DebugRecord { + route: &'static str, + effect: String, +} + +impl Observe for DebugTrace +where + E: std::fmt::Debug, + R: Route, +{ + type Error = R::Error; + + fn observe(&mut self, effect: &E) -> Result<(), Self::Error> { + self.records.push(DebugRecord { + route: std::any::type_name::(), + effect: format!("{effect:?}"), + }); + Ok(()) + } +} diff --git a/demos/examples/demo/stdlib/stack.rs b/demos/examples/demo/stdlib/stack.rs new file mode 100644 index 00000000..eda9c550 --- /dev/null +++ b/demos/examples/demo/stdlib/stack.rs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A reusable operand-stack component with invariant-preserving operations. +struct Stack { + items: Vec, +} + +impl Stack { + fn new() -> Self { + Self { items: Vec::new() } + } + + /// Load initial operands with the rightmost value treated as the top of the stack. + fn seeded(values: &[i64]) -> Self { + Self { + items: values.to_vec(), + } + } + + fn push(&mut self, value: i64) { + self.items.push(value); + } + + fn pop(&mut self) -> Result { + self.items.pop().ok_or(StackFault::Underflow) + } + + fn top(&self) -> Option { + self.items.last().copied() + } +} + +impl Supply for Stack { + type Fault = StackFault; + + fn supply(&mut self) -> Result { + self.pop() + } +} + +#[derive(Debug)] +enum StackFault { + Underflow, +} diff --git a/demos/examples/demo/vihaco/execute.rs b/demos/examples/demo/vihaco/execute.rs new file mode 100644 index 00000000..848ca59d --- /dev/null +++ b/demos/examples/demo/vihaco/execute.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// Marker message for instructions whose execution does not require a runtime-supplied message. +#[derive(Debug, Clone, Copy, Default)] +struct NoMessage; + +/// Outcome of one instruction step. This is independent of any timing model. It answers whether +/// the parent may advance the program counter or must keep the composite parked. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Execution { + /// The step resolved; the parent may advance to the next instruction. + Complete, + /// The step is unresolved; the parent must wait for a completion. + Parked, +} + +/// The standardized result of starting or resuming one instruction route. Effects are handled +/// independently from the route's completion state. +struct StepResult { + effects: Effects, + execution: Execution, +} + +/// A component executes one fully-resolved runtime instruction against its own state. +trait Execute { + type Message; + type Effect; + type Fault; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} diff --git a/demos/examples/demo/vihaco/handle.rs b/demos/examples/demo/vihaco/handle.rs new file mode 100644 index 00000000..60061bf2 --- /dev/null +++ b/demos/examples/demo/vihaco/handle.rs @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A reusable, machine-agnostic capability: this component knows how to swallow this effect. +trait Absorb { + type Fault; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; +} + +/// A non-consuming effect observer. Observers borrow effects before their semantic handler +/// consumes them and do not determine the effect's destination. +trait Observe { + type Error; + + fn observe(&mut self, effect: &Effect) -> Result<(), Self::Error>; +} + +/// Effect handling, disambiguated by `Route`. The macro normally generates implementations that +/// forward to `Absorb`. +trait Handle { + type Error; + + fn handle(&mut self, effect: Effect) -> Result<(), Self::Error>; +} diff --git a/demos/examples/demo/vihaco/machine_macro.rs b/demos/examples/demo/vihaco/machine_macro.rs new file mode 100644 index 00000000..6cded070 --- /dev/null +++ b/demos/examples/demo/vihaco/machine_macro.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +// The planned `machine!` macro will make effect fanout explicit in each runtime instruction arm: +// +// effects { +// observe foo, bar; +// to foobar; +// } +// +// It will generate calls to every listed observer followed by exactly one call to the listed +// handler. Observers borrow the effect; the handler receives ownership. diff --git a/demos/examples/demo/vihaco/resume.rs b/demos/examples/demo/vihaco/resume.rs new file mode 100644 index 00000000..743589e5 --- /dev/null +++ b/demos/examples/demo/vihaco/resume.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A component resumes a previously parked operation from an owned completion. +trait Resume { + type Effect; + type Fault; + + fn resume(&mut self, completion: C) -> Result, Self::Fault>; +} diff --git a/demos/examples/demo/vihaco/route.rs b/demos/examples/demo/vihaco/route.rs new file mode 100644 index 00000000..6b026c18 --- /dev/null +++ b/demos/examples/demo/vihaco/route.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// Compile-time identity for one instruction route selected by a composite. +/// +/// A component can implement the same instruction or effect type in several places. The route +/// marker keeps those selections distinct: `IntegerAdd` and `IntegerSub`, for example, may both +/// produce `ValueResult`, but they are still different routes with independently generated +/// message, effect, timing, and diagnostic wiring. This is also why a route marker is separate +/// from a runtime completion state: this trait describes the static dispatch path, while runtime +/// execution state describes the operation that is currently running or parked. +/// +/// The composite machinery generates one marker and one implementation for every selected route. +/// Users provide the component operations and handlers; they do not implement this trait. +trait Route { + /// Effect produced by the component on this route and passed to its observers and handlers. + /// + /// The association lets generated dispatch name the route once and derive the effect type + /// from it, rather than repeating that type throughout every generated call site. + type Effect; + + /// Error type used to normalize failures at this route's dispatch boundary. + /// + /// Component execution, message resolution, observation, and effect handling may each have + /// their own lower-level errors. Generated wiring converts those errors into the route's + /// containing-machine error type before returning from dispatch. + type Error; +} diff --git a/demos/examples/demo/vihaco/supply.rs b/demos/examples/demo/vihaco/supply.rs new file mode 100644 index 00000000..4307145a --- /dev/null +++ b/demos/examples/demo/vihaco/supply.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// The dual of `Absorb`: this component knows how to hand out this message type. +trait Supply { + type Fault; + + fn supply(&mut self) -> Result; +} diff --git a/demos/src/main.rs b/demos/src/main.rs new file mode 100644 index 00000000..448b7804 --- /dev/null +++ b/demos/src/main.rs @@ -0,0 +1,403 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +//! Earlier demo for the rewrite, keeping for "historical purposes" +//! see examples/ for a working demo + +#![allow(warnings)] + +fn main() {} + +macro_rules! machine { + ($($tokens:tt)*) => {}; +} + +macro_rules! use_as_vihaco { + () => { + use crate::vihaco_concepts as vihaco; + }; +} + +macro_rules! use_vihaco_parse { + () => { + #[allow(unused_imports)] + use vihaco_parser::Parse; + use vihaco_parser_derive::Parse; + }; +} + +mod vihaco_concepts { + use vihaco::Effects; + + pub struct NoMessage {} + pub enum NoFault {} + pub enum NoEffect {} + + pub trait Type {} + + pub trait Value { + type Type: Type; + + fn type_of(&self) -> Self::Type; + } + + pub struct BinaryOperands { + pub lhs: V, + pub rhs: V, + } + + pub trait Execute { + type Message; + type Effect; + type Fault; + + fn execute( + &mut self, + instruction: I, + message: Self::Message, + ) -> Result, Self::Fault>; + } + + pub enum Execution { + Continue, + Parked, + } + + pub trait Step { + type Instruction; + type Fault; + + fn step(&mut self, instruction: Self::Instruction) -> Result; + } +} + +mod machine { + use crate::*; + + pub struct Composite { + clock: clock::GlobalClock, + channels: channel::ChannelManager, + cpu_a: cpu::CPU, + cpu_b: cpu::CPU, + } + + mod syntax { + pub enum Instruction {} + } + + mod instruction { + pub enum Instruction {} + } +} + +mod channel { + use crate::*; + use_as_vihaco!(); + + struct Channel { + sender: u32, + receiver: u32, + } + + pub struct MessageChannel {} + + pub struct ChannelManager { + channels: Vec, + } + + pub mod syntax { + use_vihaco_parse!(); + + #[derive(Parse)] + #[syntax_class(instruction, head = "channel")] + pub enum Instruction { + Send(u32), + Recv(u32), + } + + pub struct Send {} + pub struct Recv {} + } + + pub mod instruction { + use super::*; + + pub enum Instruction { + Send(Send), + Recv(Recv), + } + + pub struct Send { + channel: u32, + } + + pub enum SendFault { + ChannelDoesNotExist, + } + + pub struct Recv { + channel: u32, + } + + impl vihaco::Execute for ChannelManager { + type Message = vihaco::NoMessage; + type Effect = vihaco::NoEffect; + type Fault = SendFault; + + fn execute( + &mut self, + instruction: Send, + message: Self::Message, + ) -> Result<::vihaco::Effects, Self::Fault> { + todo!() + } + } + } +} + +mod cpu { + use crate::*; + use_as_vihaco!(); + + enum Type { + U32, + } + + impl vihaco::Type for Type {} + + enum Value { + U32(u32), + } + + impl vihaco::Value for Value { + type Type = Type; + + fn type_of(&self) -> Self::Type { + match self { + Self::U32(..) => Type::U32, + } + } + } + + machine!( + composite CPU { + alu: arithmetic::ALU, + stack: stack::Stack, + clock: clock::LocalClock, + channels: channel::MessageChannel, + } + + syntax { + Add <= arithmetic::syntax::Add, + Sub <= arithmetic::syntax::Sub, + Mul <= arithmetic::syntax::Mul, + Send <= channel::syntax::Send, + Recv <= channel::syntax::Recv, + } + + runtime { + ![0x01] + Add => arithmetic::instruction::Add { + message from stack; + effects to stack; + } + + ![0x02] + Sub => arithmetic::instruction::Sub { + message from stack; + effects to stack; + } + + ![0x03] + Mul => arithmetic::instruction::Mul { + message from stack; + effects to stack; + } + + ![0x04] + Send => channel::instruction::Send { + message from channels; + effects to channels, clock; + } + + ![0x05] + Recv => channel::instruction::Send { + message from channels; + effects to channels, clock; + } + } + ); + + pub struct CPU { + alu: arithmetic::ALU, + stack: stack::Stack, + clock: clock::LocalClock, + channels: channel::MessageChannel, + } + + pub enum CPUInstruction { + Add(arithmetic::instruction::Add), + Sub(arithmetic::instruction::Sub), + Mul(arithmetic::instruction::Mul), + Send(channel::instruction::Send), + Recv(channel::instruction::Recv), + } +} + +mod stack { + pub struct Stack { + stack: Vec, + } + + impl Stack { + pub fn push(&mut self, value: T) { + self.stack.push(value); + } + + pub fn pop(&mut self) -> Option { + self.stack.pop() + } + } +} + +mod clock { + struct Timeline {} + + pub struct GlobalClock { + timeline: Timeline, + } + + pub struct LocalClock {} +} + +mod arithmetic { + use std::marker::PhantomData; + use_as_vihaco!(); + + pub struct ALU { + _marker: PhantomData V>, + } + + pub mod syntax { + use_vihaco_parse!(); + + #[derive(Parse)] + #[syntax_class(instruction, head = "arith")] + pub enum Instruction + where + Ty: for<'a> Parse<'a>, + { + Add(Ty), + Sub(Ty), + Mul(Ty), + } + + pub struct Add {} + pub struct Sub {} + pub struct Mul {} + } + + pub mod instruction { + use super::*; + use ::vihaco::effect::Effects; + + pub enum Instruction { + Add(Add), + Sub(Sub), + Mul(Mul), + } + + pub struct Add { + ty: Ty, + } + + pub trait TryAdd: vihaco::Value { + type Result; + type Fault; + + fn try_add(&self, other: Self, ty: Self::Type) -> Result; + } + + impl vihaco::Execute> for ALU + where + V: TryAdd, + { + type Message = vihaco::BinaryOperands; + type Effect = effect::ValueResult; + type Fault = V::Fault; + + fn execute( + &mut self, + instruction: Add, + message: Self::Message, + ) -> Result, Self::Fault> { + let value = message.lhs.try_add(message.rhs, instruction.ty)?; + Ok(Effects::One(effect::ValueResult { value })) + } + } + + pub struct Sub { + ty: Ty, + } + + pub trait TrySub: vihaco::Value { + type Result; + type Fault; + + fn try_sub(&self, other: Self, ty: Self::Type) -> Result; + } + + impl vihaco::Execute> for ALU + where + V: TrySub, + { + type Message = vihaco::BinaryOperands; + type Effect = effect::ValueResult; + type Fault = V::Fault; + + fn execute( + &mut self, + instruction: Sub, + message: Self::Message, + ) -> Result, Self::Fault> { + let value = message.lhs.try_sub(message.rhs, instruction.ty)?; + Ok(Effects::One(effect::ValueResult { value })) + } + } + + pub struct Mul { + ty: Ty, + } + + pub trait TryMul: vihaco::Value { + type Result; + type Fault; + + fn try_mul(&self, other: Self, ty: Self::Type) -> Result; + } + + impl vihaco::Execute> for ALU + where + V: TryMul, + { + type Message = vihaco::BinaryOperands; + type Effect = effect::ValueResult; + type Fault = V::Fault; + + fn execute( + &mut self, + instruction: Mul, + message: Self::Message, + ) -> Result, Self::Fault> { + let value = message.lhs.try_mul(message.rhs, instruction.ty)?; + Ok(Effects::One(effect::ValueResult { value })) + } + } + } + + mod effect { + pub struct ValueResult { + pub value: V, + } + } +} diff --git a/vision/clock.md b/vision/clock.md index 53e25147..c8cc4aaa 100644 --- a/vision/clock.md +++ b/vision/clock.md @@ -37,39 +37,41 @@ Questions: ## Updated Direction The material above records the questions that motivated the clock design. The current direction is -defined together with the architecture mapped in [`contents.md`](./contents.md) and the two-CPU -integration target in [`demo.md`](./demo.md). +defined together with the architecture mapped in [`contents.md`](./contents.md), the execution +pipeline in [`execution-pipeline.md`](./execution-pipeline.md), and the two-CPU integration target +in [`../demos/examples/demo.md`](../demos/examples/demo.md). -A clock is not a universal vihaco authority and does not replace instruction dispatch, resource -handling, or the driver. Clock implementations are reusable library components built through the -same component and effect model as stacks, arithmetic units, and communication resources. Vihaco -core supplies the boundaries that let those components participate: +A clock is not a universal vihaco authority. Clock implementations are reusable library components +built through the same component and effect model as stacks, arithmetic units, and communication +resources. Vihaco core supplies the boundaries that let them participate: -- A composite executes one supplied runtime instruction through `step`. +- An executable child composite performs one supplied runtime instruction through `step`. - Routes may associate execution with timing information. -- Effects can be handled by local components and propagated across nested composites. -- A step returns owned status and driver-facing work. +- Effects can be handled by local clocks and propagated across nested composites. +- A child step returns owned status and root-facing work. - Parked execution registers owned continuation state. -- An external driver selects the next eligible work. +- The top-level runtime root selects and dispatches the next event. The two-CPU demo chooses one concrete arrangement: ```text -Runtime -├── TimelineDriver -└── HeterogeneousMachine - ├── GlobalClock - ├── reusable communication component - ├── CpuA - │ └── LocalClock { global_ticks_per_local_cycle: 2 } - └── CpuB - └── LocalClock { global_ticks_per_local_cycle: 3 } +HeterogeneousMachine +├── GlobalClock +├── reusable communication component +├── CpuA +│ └── LocalClock { global_ticks_per_local_cycle: 2 } +└── CpuB + └── LocalClock { global_ticks_per_local_cycle: 3 } ``` -`GlobalClock` is modeled state inside the top-level composite. `TimelineDriver` remains external so -it can use the clock and CPU state without a field borrowing its containing machine. Another -runtime may place its global clock state inside the driver instead. Clock placement is a runtime -choice, not part of the `Instruction` or `Execute` contracts. +`HeterogeneousMachine` is both the top-level composite and the concrete runtime root. It has no +local executable instruction section or program. Its inherent `run` loop removes owned events from +`GlobalClock`, dispatches them into the appropriate child or resource, and returns owned scheduling +requests to the clock. + +The initial implementation deliberately does not introduce an external `TimelineDriver`, +`Driver` trait, or `Runtime` wrapper. Those abstractions can be revisited after a second +runtime demonstrates a different orchestration policy and a stable shared boundary. ## Time, Duration, and Local Cycles @@ -79,7 +81,7 @@ The model distinguishes three quantities: - **Global duration** is a distance between two global ticks. - **Local cycles** count work in the domain of one child clock. -They should not be interchangeable integers. Conceptually: +They should not be interchangeable integers: ```rust pub struct GlobalTick(pub u128); @@ -88,8 +90,8 @@ pub struct LocalCycles(pub u64); ``` The exact representation remains a library API decision. Distinct types prevent an absolute time -from being used as a duration and prevent one CPU's local cycles from being mistaken for global -ticks. Arithmetic that advances time or converts cycles must detect overflow rather than silently +from being used as a duration and prevent local cycles from being mistaken for global ticks. +Arithmetic that advances time or converts cycles must detect overflow rather than silently wrapping. Host execution time has no relationship to modeled time. A slow Rust call can represent zero @@ -97,13 +99,12 @@ modeled duration, while a fast call can schedule work far into the future. ## Global Clock -The global clock is the definitive time authority for a particular modeled machine. In the demo it -owns: +The global clock is the definitive time authority for the demo. It owns: - The current `GlobalTick`. - An ordered collection of future events. - A monotonically increasing sequence used to order events at the same tick. -- Any generation or reset state required to reject stale work. +- Any generation or reset state required to reject stale scheduled work. It does not: @@ -111,12 +112,10 @@ It does not: - Advance a program counter. - Borrow a CPU and call its `step` method. - Interpret arithmetic, communication, or other domain effects. -- Observe every mutation made by every component. - -Those responsibilities belong to the driver, the configured program-counter owner, and typed -effect handlers. +- Know the private fields or concrete type of `HeterogeneousMachine`. +- Call back into its containing composite. -The global clock can be generic over the event type used by a library or machine: +The clock is generic over its event type: ```rust pub struct Scheduled { @@ -135,20 +134,71 @@ The first implementation is event-driven. It advances directly to the next sched than visiting every empty global tick: ```text -remove the earliest event +remove the earliest owned event -> advance GlobalClock.now to its tick - -> return the owned event to the driver - -> driver performs the selected work - -> insert resulting events + -> return the owned event to HeterogeneousMachine + -> root dispatches the selected child or completion + -> root returns owned scheduling requests + -> insert those requests into GlobalClock -> repeat ``` Skipped ticks remain meaningful positions on the timeline; they simply contain no observable work. +## Root Event Loop and Rust Ownership + +`HeterogeneousMachine` owns the machine-specific event sum: + +```rust +pub enum CpuEvent { + RunNext, + Resume(ContinuationId), +} + +pub enum MachineEvent { + CpuA(CpuEvent), + CpuB(CpuEvent), + Deliver(Delivery), +} +``` + +The concrete variants may change as communication handling becomes concrete. Vihaco core does not +define them. The reusable CPU produces only `CpuEvent`; parent routing wraps it in the variant for +the child instance that produced it. + +A representative root loop is: + +```rust +impl HeterogeneousMachine { + pub fn run(&mut self) -> eyre::Result { + self.initialize_timeline()?; + + loop { + let Some(scheduled) = self.global_clock.pop_next()? else { + return self.classify_empty_timeline(); + }; + + let requests = + self.dispatch_event(scheduled.at, scheduled.event)?; + + self.global_clock.extend(requests)?; + } + } +} +``` + +`pop_next` returns an owned event. The mutable borrow of `self.global_clock` therefore ends before +`dispatch_event` borrows a child or another root field. Dispatch returns owned requests, which are +inserted only after child execution and parent-level effect handling complete. + +`GlobalClock` must not solve the ownership problem by receiving a closure or reference that reaches +back into `HeterogeneousMachine`. The direction of control remains root-to-clock and +root-to-child. + ## Local Clocks -A local clock relates child execution to the global timeline. It is not an independent time -authority. The demo begins with a fixed integer ratio: +A local clock relates child execution to the global timeline. It is not an independent event queue +or definitive time authority. The demo begins with a fixed integer ratio: ```rust pub struct LocalClock { @@ -174,63 +224,26 @@ CpuA: 1 local cycle × 2 = 2 global ticks CpuB: 1 local cycle × 3 = 3 global ticks ``` -Both CPUs may therefore execute the same `add` runtime instruction through the same reusable -arithmetic component and report one local cycle, while becoming eligible at different global -ticks. +Both CPUs may execute the same `add` runtime instruction through the same reusable arithmetic +component and report one local cycle while becoming eligible at different global ticks. -A local clock may be an ordinary component and typed handler. It can accept route completion -information, update its local cycle count, and produce an owned global scheduling request. A debug +A local clock is an ordinary component and typed handler. It can accept route-completion +information, update its local cycle count, and produce an owned converted delay. The containing CPU +route combines that delay with child-local next work to form `Schedule`. A debug component may handle the same completion information for tracing. Both use the same typed handler model. Child clocks do not advance private timelines and later reconcile them with the parent. Their -converted work is scheduled directly on the common global timeline, so global event ordering +converted work is submitted directly to the common global timeline, so global event ordering defines how child execution interleaves. -The fixed integer ratio is sufficient for the integration demo. Rational periods, phase offsets, -drift, and clock-domain crossings can be library extensions after this model is proven. - -## Clock and Driver Roles - -A clock and a driver answer different questions: - -| Question | Owner in the demo | -|---|---| -| What is the current definitive tick? | `GlobalClock` | -| Which event is earliest? | `GlobalClock` event ordering | -| Which work does that event represent? | The machine-specific event type | -| Who obtains the corresponding instruction or completion? | `TimelineDriver` through explicit machine operations | -| Who calls `step`? | `TimelineDriver` | -| Who applies returned scheduling requests? | `TimelineDriver`, by inserting them into `GlobalClock` | -| Who advances a CPU program counter? | The CPU's modeled program-counter component | - -The driver loop is: - -```text -read the earliest event from GlobalClock - -> advance global time - -> identify the target CPU or completion - -> obtain an owned runtime instruction or completion - -> call the top-level machine route - -> interpret Complete, Parked, terminal control, and scheduling work - -> return future events to GlobalClock -``` - -The driver must not retain a reference borrowed from a child program while mutably stepping the -whole machine. A CPU-owned program source therefore returns an owned runtime instruction, or the -immutable program is stored outside the mutable composite. - -A clock can itself fill the driver role in another runtime when it is external to the machine and -owns both event selection and the driving loop. The demo keeps the roles separate because its -global clock is explicitly a field of the top-level composite. - -Vihaco must also support drivers with no clock. A sequential interpreter or direct caller can -invoke `step` without modeled time. The existence of `GlobalClock` and `LocalClock` library types -does not make clocks a requirement for a composite. +If implementation shows that a local clock is only a pure fixed-ratio multiplication helper, its +configuration may later move into `GlobalClock` without changing vihaco core. The first demo keeps +local clocks as components to exercise nested timing and effect propagation. ## Instruction Timing -Runtime instructions describe semantic operations. They do not own a clock, event queue, driver, or +Runtime instructions describe semantic operations. They do not own a clock, event queue, or universal timing trait. The same `Add` type can have different duration in different routes or machines. @@ -241,8 +254,8 @@ Timing information may come from: - Runtime instruction data. - A component result. - Resource state. -- Driver configuration. -- An external completion event. +- Root runtime configuration. +- A completion event. The initial demo uses route-level local duration: @@ -255,8 +268,8 @@ successful recv -> 1 local cycle ``` This information does not belong in the reusable arithmetic component. After the route completes, -the selected local clock translates its local duration and emits driver-facing global scheduling -work. +the selected local clock translates its local duration and emits root-facing global scheduling +work: ```text runtime instruction @@ -268,8 +281,8 @@ runtime instruction ``` An instruction that mutates its component and returns `Effects` still receives route -timing. The global clock does not need to observe the mutation or every effect. It only receives the -information required to determine global eligibility. +timing. The global clock does not need to observe the mutation or every effect. It only receives +the information required to determine global eligibility. A `Tick` trait implemented by every instruction is not required. If repeated timing APIs become useful after the first implementation, they can describe route or runtime timing without coupling @@ -277,11 +290,7 @@ semantic instruction types to one clock model. ## Scheduling Requests -Scheduling work that affects an external driver must cross the `step` boundary as owned data, or be -stored in explicit machine state that the driver drains. Returning owned requests is the clearest -initial model. - -Conceptually, a request identifies when and what becomes eligible: +Scheduling work that leaves a child step crosses the boundary as owned data: ```rust pub struct Schedule { @@ -290,14 +299,16 @@ pub struct Schedule { } ``` -The driver submits the request to `GlobalClock`. The clock converts `after` to an absolute tick -relative to its current `now`, validates the arithmetic, assigns a deterministic sequence, and -inserts the event. An alternative request may already contain an absolute tick when that time comes -from an external source. +The reusable CPU returns `Schedule` and does not name its parent field or construct a +root event. `HeterogeneousMachine` maps it into `Schedule` by wrapping the event with +`MachineEvent::CpuA` or `MachineEvent::CpuB`, then submits it to `GlobalClock`. The clock converts +`after` to an absolute tick relative to its current `now`, validates the arithmetic, assigns a +deterministic sequence, and inserts the event. An alternative request may already contain an +absolute tick when that time comes from a modeled resource. -The concrete event sum is machine- or library-specific. Vihaco core does not define `RunCpu`, -`DeliverValue`, or other demo events. It only needs an owned step boundary through which the -configured runtime can communicate scheduling work. +The concrete event sum is machine-specific. Vihaco core does not define CPU instance, delivery, or +resume events. It only needs an owned child-step boundary through which the configured runtime can +communicate scheduling work. Scheduling the past is an error. Scheduling at the current tick is allowed when same-tick sequence ordering defines when the new event becomes visible. @@ -313,26 +324,27 @@ pub enum Execution { } ``` -This is the minimal status; the actual step outcome may also contain terminal control and -driver-facing work. +This is the minimal status; the actual child outcome may also contain terminal control, +continuation identity, and root-facing work. -`Complete` means the instruction and all immediate effect handling have reached a step boundary. If -the program has another instruction, its route normally returns scheduling work based on the local -duration. If the program is exhausted, the CPU leaves the runnable set instead. +`Complete` means the instruction and all immediate effect handling reached a child-step boundary. +If the program has another instruction, its route normally returns scheduling work based on the +local duration. If the program is exhausted, the CPU leaves the runnable set instead. -`Parked` means the resource or component has atomically registered an owned continuation and the -driver must not schedule the CPU's next instruction. Parking is a readiness decision, not an -unknown duration added to an otherwise complete instruction. +`Parked` means the resource or component atomically registered an owned continuation and the root +must not schedule the CPU's next instruction. Parking is a readiness decision, not an unknown +duration added to an otherwise complete instruction. When a completion becomes available: 1. A library handler identifies the parked CPU and continuation. -2. The parent routes the owned completion to that child. +2. The root routes the owned completion to that child. 3. The continuation applies its result. 4. The child's local clock accounts for the completion duration. -5. A global event makes the CPU eligible after the converted duration. +5. An owned scheduling request re-enters `GlobalClock`. -No borrow from resolution, execution, or effect handling survives the parked step. +No borrow from resolution, execution, effect handling, program fetch, or clock access survives the +parked step. ## Communication Timing @@ -384,9 +396,11 @@ The demo uses two levels of composite routing: ```text CpuA Add completes with 1 local cycle -> CpuA LocalClock converts it to 2 global ticks - -> owned scheduling request leaves CpuA - -> HeterogeneousMachine returns it to TimelineDriver - -> TimelineDriver inserts CpuA eligibility into GlobalClock + -> CpuA route combines the delay with CpuEvent::RunNext + -> owned Schedule leaves CpuA + -> HeterogeneousMachine maps it to MachineEvent::CpuA + -> HeterogeneousMachine submits it to GlobalClock + -> GlobalClock schedules CpuA eligibility ``` `CpuB` follows the same path but converts one local cycle to three global ticks. @@ -394,21 +408,23 @@ CpuA Add completes with 1 local cycle A communication completion follows the inverse direction: ```text -GlobalClock releases delivery event - -> TimelineDriver routes the owned event through HeterogeneousMachine - -> communication handler identifies the waiting CPU - -> parent forwards the completion into the child +GlobalClock releases an owned delivery event + -> HeterogeneousMachine routes it to the communication handler + -> handler identifies the waiting CPU + -> root forwards the completion into the child -> child continuation completes recv - -> LocalClock schedules the child's next eligibility globally + -> LocalClock converts the receive duration + -> child route emits the next Schedule + -> HeterogeneousMachine inserts it into GlobalClock ``` The framework preserves nested route identity and ownership. Clock and communication libraries -define the event contents and resource behavior. +define event contents and resource behavior; the root defines the machine-specific event dispatch. ## Demonstration Trace -The trace in [`demo.md`](./demo.md) is the acceptance case for the clock model. Its important timing -points are: +The trace in [`../demos/examples/demo.md`](../demos/examples/demo.md) is the acceptance case for +the clock model: ```text global 0: CpuA add; next eligible at 2 @@ -428,6 +444,7 @@ This proves: - The global event order is definitive and deterministic. - A parked receive removes a CPU from normal instruction scheduling. - Delivery resumes the correct continuation and re-enters the timeline through its local clock. +- The root can coordinate executable children without a local executable instruction section. ## Ownership Boundaries @@ -435,17 +452,17 @@ The demo assigns ownership as follows: | Owner | State and policy | |---|---| -| Vihaco core | Typed instructions, execution relationships, effects, route generation, step status, and owned driver boundary | -| `GlobalClock` library component | Current global tick, event queue, sequence allocation, and reset generation | +| Vihaco core | Typed instructions, execution relationships, effects, route generation, child-step status, and owned nested boundaries | +| `GlobalClock` library component | Current global tick, event queue, sequence allocation, checked scheduling, and reset generation | | `LocalClock` library component | Local cycle state and local-to-global conversion policy | -| `TimelineDriver` library item | The loop that selects events, invokes machine work, and applies scheduling requests | +| `HeterogeneousMachine` runtime root | Machine event sum, event dispatch, parent effect routing, completion routing, termination, and deadlock detection | | CPU composite | Local architectural state, selected instruction routes, program, program counter, and parked status | | Communication library | Values in flight, waiting continuations, acceptance, delivery, and transport timing | | Runtime instruction | Fully resolved semantic operands | -Instructions do not own clocks, queues, wakers, or scheduler state. The global clock does not own -component semantics or instruction dispatch. The driver does not mutate private fields directly; -it uses explicit machine operations. +Instructions do not own clocks, queues, wakers, or scheduler state. `GlobalClock` does not own +component semantics or instruction dispatch. The root accesses children and resources through +explicit operations rather than giving the clock access to private fields. ## Faults, Reset, and Deadlock @@ -459,33 +476,35 @@ Clock and scheduling faults retain enough context to identify: Reset invalidates pending work through a generation or equivalent identity. A completion created before reset cannot resume a newly reset CPU that happens to reuse the same local identifier. +Reset clears and reseeds the global queue consistently with child program, cursor, local clock, +communication, and continuation state. -The driver detects deadlock when: +`HeterogeneousMachine::run` detects deadlock when: - No runnable CPU remains. - Every incomplete CPU is parked. - The global event queue contains no event capable of satisfying a continuation. -Deadlock is distinct from successful program exhaustion and from waiting on an external event that -the selected driver knows may still arrive. +Deadlock is distinct from successful program exhaustion. Waiting for an external event is deferred +until a future runtime provides a concrete external completion source. ## Implementation Sequence Clock work should develop alongside the instruction rewrite and demo: 1. Define distinct global tick, global duration, and local cycle types. -2. Implement a deterministic generic global event queue with checked time arithmetic. +2. Implement a deterministic generic `GlobalClock` with checked time arithmetic. 3. Implement fixed-ratio local clock conversion. -4. Add owned scheduling work to the composite step outcome. -5. Drive one clocked CPU through route-local timing. +4. Add owned root-facing scheduling work to child outcomes. +5. Drive one clocked CPU from a small root event loop. 6. Place two CPU instances under one global clock and verify the two ratios. 7. Add library-defined send delivery. 8. Add parked receive, owned completion, wakeup, and stale-generation protection. -9. Assert the deterministic trace from `demo.md`. +9. Assert the deterministic trace from `../demos/examples/demo.md`. -Each stage should leave a focused runnable test. The concrete `GlobalClock`, `LocalClock`, and -`TimelineDriver` APIs may begin as ordinary library types. Common traits or macro shorthand should -be introduced only after these implementations expose stable repetition. +Each stage should leave a focused runnable test. The concrete `GlobalClock`, `LocalClock`, root +event sum, and inherent run loop should begin as ordinary Rust. Common traits or macro shorthand +should be introduced only after these implementations expose stable repetition. ## Acceptance Criteria @@ -496,6 +515,11 @@ The clock model is ready for the integration demo when: - Global time advances monotonically. - Same-tick events execute in deterministic sequence order. - Empty spans can be skipped without changing results. +- `GlobalClock` is generic over an owned event type. +- `GlobalClock` never calls back into its containing runtime root. +- The clock borrow ends before the root mutably steps a child. +- Child-local events acquire CPU instance identity only when the parent maps them into the root + event sum. - `CpuA` converts one local cycle to two global ticks. - `CpuB` converts one local cycle to three global ticks. - The same arithmetic instruction can have different global duration without knowing either clock. @@ -503,16 +527,20 @@ The clock model is ready for the integration demo when: - A parked route schedules no next instruction. - A communication completion resumes only its registered continuation. - Resume timing passes through the waiting CPU's local clock. -- Program exhaustion, deadlock, parking, and external waiting are distinguishable. +- Program exhaustion, deadlock, and parking are distinguishable. - Reset prevents stale scheduled work from mutating a new execution generation. - The global clock remains an ordinary library component rather than a required vihaco core concept. -- A clockless sequential driver can use the same `step` boundary. +- The root coordinates executable children without a local executable instruction section or + `Step` implementation. ## Deferred Questions The first implementation does not need to decide: +- A general driver or runtime-wrapper abstraction. +- Interchangeability between timeline, sequential, real-time, and external-hardware runtimes. +- External completion polling or waiting. - Fractional or irrational clock ratios. - Phase offsets and clock drift. - Multiple visibility phases within one global tick. @@ -522,8 +550,7 @@ The first implementation does not need to decide: - Dynamic clock-tree reconfiguration. - General cancellation of in-flight operations. -These features may extend the library-level clock and driver implementations later. They do not -change the ownership boundaries defined by [`instruction-model.md`](./instruction-model.md), -[`execution-pipeline.md`](./execution-pipeline.md), and -[`runtime-drivers.md`](./runtime-drivers.md), or the integration behavior required by -[`demo.md`](./demo.md). +These features may extend or replace the concrete root loop after another runtime provides evidence +for the right boundary. They do not change the ownership boundaries defined by +[`execution-pipeline.md`](./execution-pipeline.md), or the integration behavior required by +[`../demos/examples/demo.md`](../demos/examples/demo.md). diff --git a/vision/contents.md b/vision/contents.md index 8fb3d0cf..a5a09587 100644 --- a/vision/contents.md +++ b/vision/contents.md @@ -1,51 +1,34 @@ # Vihaco Vision Contents -This directory describes the in-progress vihaco architecture and the reference machine used to -validate it. The current-direction documents below should be read together: each owns a distinct -part of the design, while the demo provides the integration target. +This directory describes the in-progress vihaco architecture. The concrete reference-machine +documents now live beside the example under `demos/examples/`; the documents below should be read +together, with the demo providing the integration target. ## Architecture Read these documents in order when following the instruction rewrite from its type model through runtime execution: -1. [`instruction-model.md`](./instruction-model.md) defines surface and runtime instruction - products, `Instruction` and `Execute`, component responsibilities, composite selection, and - generated machine instruction sums. -2. [`types-and-values.md`](./types-and-values.md) defines author-owned data models, scalar parser +1. [`types-and-values.md`](./types-and-values.md) defines author-owned data models, scalar parser and encoding support, surface/runtime type and value staging, cross-component compatibility, explicit conversion, and future bytecode encoding. -3. [`execution-pipeline.md`](./execution-pipeline.md) defines surface resolution and the +2. [`execution-pipeline.md`](./execution-pipeline.md) defines surface resolution and the route-specific runtime stages of message resolution, component execution, and effect handling. -4. [`runtime-drivers.md`](./runtime-drivers.md) defines step outcomes, program drivers, clock and - driver roles, program-counter ownership, parking, resumption, and fault boundaries. -5. [`stack-machine-policy.md`](./stack-machine-policy.md) applies the ownership model to native +3. [`stack-machine-policy.md`](./stack-machine-policy.md) applies the ownership model to native stack operations, arithmetic, locals, heap allocation, printing, calls, and control flow. -6. [`sst-resolution.md`](./sst-resolution.md) defines pattern-based SST parsing, surface-to-runtime +4. [`sst-resolution.md`](./sst-resolution.md) defines pattern-based SST parsing, surface-to-runtime resolution, canonical syntax ownership, and the generated composite parser. -7. [`macro-generation.md`](./macro-generation.md) separates what instruction, component, +5. [`macro-generation.md`](./macro-generation.md) separates what instruction, component, composite, and effect-wiring macros generate from what machine authors write. -8. [`design-tradeoffs.md`](./design-tradeoffs.md) records the alternatives considered and the - architecture's observability, debugging, and error-model consequences. -9. [`implementation-plan.md`](./implementation-plan.md) defines test coverage, migration phases, - focused architecture fixtures, deferred questions, and acceptance criteria. +6. [`demo-vihaco-concepts.md`](../demos/examples/demo-vihaco-concepts.md) explains every contract + in the demo's `vihaco` layer with independent examples, including execution, message supply, + effect routing, suspension, route identity, and the planned effect-fanout macro. ## Reference Machine and Timing -- [`demo.md`](./demo.md) is the end-to-end integration target: two reusable CPU composites with - different local clock ratios exchange arithmetic results through a reusable communication - component. +- [`demo.md`](../demos/examples/demo.md) is the end-to-end integration target: two reusable CPU + composites with different local clock ratios exchange arithmetic results through a reusable + communication component under a non-executing clock-driven root. - [`clock.md`](./clock.md) defines the timeline model needed by that demo, including global and - local clocks, deterministic scheduling, driver interaction, parking, communication timing, and - reset behavior. The material above its divider records the earlier questions that motivated the - current design. - -## Earlier Working Notes - -- [`vision.md`](./vision.md) is an early, incomplete architecture sketch. It provides historical - context but includes proposals superseded by the current-direction documents. -- [`traits.md`](./traits.md) is an earlier first-class-traits draft. Its instruction ownership and - capability ideas are exploratory rather than the current implementation plan. - -When these earlier notes conflict with the architecture, reference-machine, or timing documents -above, the current-direction documents take precedence. + local clocks, deterministic root event dispatch, parking, communication timing, and reset + behavior. diff --git a/vision/demo.md b/vision/demo.md deleted file mode 100644 index 3e881134..00000000 --- a/vision/demo.md +++ /dev/null @@ -1,467 +0,0 @@ -# Heterogeneous Two-CPU Demo - -## Purpose - -The vihaco integration reference is a small heterogeneous computer built from reusable parts: - -- One top-level composite owns a definitive global clock. -- The composite contains two CPU composites. -- Each CPU owns a local stack, arithmetic state, a local clock, a program, and a program counter. -- Both CPUs expose `add`, `sub`, `mul`, `send`, and `recv`. -- The CPUs exchange arithmetic results through a shared communication component. -- The two local clocks map their cycles to the global clock at different rates. - -This demo is the concrete forcing case for the architecture mapped in -[`contents.md`](./contents.md). Those documents define the general instruction, component, -composite, effect, and driver boundaries. This document defines a machine that must be expressible -through those boundaries without adding CPU-, channel-, or clock-specific exceptions to vihaco's -core. - -This is the only end-to-end reference runtime. Smaller machines may remain as conformance fixtures -for individual instruction and driver boundaries, but they do not define a competing integration -target. - -The goal is not merely to make the example run. The goal is to show that vihaco supports fast and -correct prototyping of heterogeneous machines by composing ordinary Rust types, selecting a precise -instruction set, and changing configuration rather than rewriting execution logic. - -## Machine Topology - -The demo runtime has an external driver and one top-level machine: - -```text -Runtime -├── TimelineDriver -└── HeterogeneousMachine - ├── GlobalClock - ├── shared communication component - ├── CpuA - │ ├── program and program counter - │ ├── operand stack - │ ├── arithmetic unit - │ ├── communication endpoint - │ └── LocalClock { global_ticks_per_local_cycle: 2 } - └── CpuB - ├── program and program counter - ├── operand stack - ├── arithmetic unit - ├── communication endpoint - └── LocalClock { global_ticks_per_local_cycle: 3 } -``` - -`HeterogeneousMachine` is the single top-level composite. `CpuA` and `CpuB` are two instances of -the same reusable CPU composition unless the implementation reveals a genuine need for different -CPU types. Their instruction semantics are identical. Their clock configuration, programs, local -state, and route identities are distinct. - -The global clock is part of the modeled machine, but it does not call back into its containing -composite. `TimelineDriver` remains external to the machine so it can use the machine's state -without creating a self-borrowing driver field. The driver asks the machine for its next scheduled -work, invokes the appropriate child step, and returns any resulting scheduling work to the global -clock. - -This arrangement intentionally separates: - -- The global clock, which owns the definitive modeled time and event ordering. -- The local clocks, which translate local cycles into global duration. -- The driver, which repeatedly selects eligible work and calls `step`. -- The CPUs, which own their local execution state. - -## Framework, Library, and Demo Boundaries - -The demo uses channel and clock concepts, but those concepts do not become intrinsic vihaco -semantics. Vihaco provides the composition mechanisms; reusable libraries provide particular -machine components. - -| Layer | Responsibilities | -|---|---| -| Vihaco core | Surface/runtime instruction separation, `Resolve`, `Execute`, generated route dispatch, typed effects and handlers, nested composite boundaries, owned step outcomes, parking, and driver integration | -| Reusable component libraries | Stacks, arithmetic units, program storage, program counters, local and global clocks, timeline scheduling, channel endpoints, and a shared channel fabric | -| Demo machine | Selects the five instructions, instantiates two CPUs, assigns clock ratios, wires communication, loads the programs, and chooses initial stack values | - -`ChannelFabric` is therefore an example library component, not a vihaco-level idea. The same is true -of a particular mailbox, interconnect, clock, stack, or arithmetic implementation. Such types may -ship with the vihaco project as useful libraries, but the framework must not contain special cases -for their names or semantics. - -The first demo uses `i64` directly for its operand stacks, arithmetic messages/results, and channel -payloads. It does not need a heterogeneous value enum or runtime type descriptor. This is a -deliberate acceptance case for the author-defined data-model boundary: adding the demo must not -reintroduce a vihaco `Value` or `Type`. Focused heap or mixed-value fixtures may define their own -carrier independently. See [`types-and-values.md`](./types-and-values.md). - -The core requirement is more general: - -- A nested composite can emit an owned effect across its parent boundary. -- The parent can route that effect to any typed handler. -- A handler can later produce an owned completion for the correct child. -- A parked child can resume from that completion. -- Driver-facing scheduling work can leave `step` without retaining borrows. - -A different communication library should be usable without changing vihaco's instruction or -composite machinery. - -## CPU Instruction Set - -Both CPUs select the same five surface and runtime operations: - -```text -add -sub -mul -send -recv -``` - -The CPU composite owns the machine-local routes. Merely containing an arithmetic unit, stack, local -clock, or communication endpoint does not expose every operation offered by those components. - -### Arithmetic - -`add`, `sub`, and `mul` use the same staged path: - -```text -resolve: - consume rhs and lhs from the CPU's local operand stack - -execute: - run the selected reusable arithmetic operation - -handle: - push the result onto the same CPU's local operand stack - account for one local cycle -``` - -The arithmetic component does not know which CPU contains it, which stack supplied the values, or -how long a local cycle lasts globally. The same instruction and component implementations execute -in both CPUs. In the first demo their shared boundary type is `i64`, so no cross-component cast is -performed. - -Each arithmetic route initially costs one local cycle. Because the local clocks have different -ratios, the same semantic operation has different global duration: - -```text -CpuA add: 1 local cycle × 2 global ticks = 2 global ticks -CpuB add: 1 local cycle × 3 global ticks = 3 global ticks -``` - -The same conversion applies to `sub` and `mul` in the first version. Later timing models may assign -different local durations per route without changing arithmetic semantics. - -### Send - -`send` consumes a value from the CPU's local stack and targets a communication component supplied -by a reusable library: - -```text -resolve: - consume the value from the local operand stack - use the resolved channel identifier from the runtime instruction - -execute: - validate or prepare the send through the CPU's communication endpoint - -handle: - emit an owned transmission request across the CPU boundary - route it through the parent to the shared communication component - account for one local cycle -``` - -The surface form may contain a symbolic channel name. The machine's `Resolve` implementation turns -that name into the runtime identifier used by the communication library. - -### Receive - -`recv` either obtains a queued value or parks: - -```text -value available: - receive the value - push it onto the local operand stack - account for one local cycle - complete - -value unavailable: - register an owned continuation - emit an owned receive request - return Parked -``` - -When a matching value arrives, the communication library produces an owned completion containing -enough identity to select the CPU and continuation. The parent routes that completion to the parked -CPU, the receive result is placed on its stack, and its local clock determines when the CPU becomes -runnable again. - -No borrow from message resolution, component execution, or effect handling survives the parked -step. - -## Nested Effect Flow - -The demo requires nested composites to communicate with sibling resources without reaching through -their parent's fields. - -For a send from `CpuA` to `CpuB`: - -```text -CpuA Send route - -> transmission effect leaves CpuA - -> HeterogeneousMachine preserves CpuA route identity - -> shared communication handler accepts the effect - -> handler queues or delivers the value - -> completion is routed to CpuB when required -``` - -For a parked receive: - -```text -CpuA Receive route - -> continuation is registered inside CpuA or its endpoint - -> receive request leaves CpuA - -> shared communication handler records the waiter - -> CpuA returns Parked - -> a later send satisfies the waiter - -> owned completion is routed back to CpuA - -> CpuA becomes eligible on the global timeline -``` - -This is ordinary typed effect handling at two composite levels. The framework does not need to know -that the effect represents a channel operation. It only needs to preserve route provenance, -deterministic handler order, ownership across suspension, and the distinction between internal and -driver-facing work. - -The first implementation may use direct generated match arms for this propagation. A generalized -hierarchical effect API is only necessary if the concrete demo reveals repeated code that cannot be -expressed cleanly by the composite declaration. - -## Timing Model - -The global clock is the definitive source of modeled time. Its event queue orders work by: - -```text -(global_tick, deterministic_sequence) -``` - -The sequence value gives stable ordering to events scheduled for the same global tick. Host -execution time never contributes to modeled duration. - -Each CPU route produces or is associated with a duration in local cycles. The selected CPU's local -clock translates that duration into a global scheduling request: - -```text -route completes with local duration - -> local clock applies its configured ratio - -> owned global scheduling request leaves the CPU - -> global clock schedules the CPU's next eligible step -``` - -The timing contract must make the following cases explicit: - -- A completed instruction schedules the CPU's next instruction after its converted duration. -- A parked `recv` does not schedule the next instruction. -- A delivery wakes only the matching continuation. -- Completing a parked receive incurs its configured local duration before the following instruction - becomes eligible. -- Program exhaustion removes the CPU from the runnable set. -- Events at the same global tick use deterministic ordering. - -The communication library owns its transport policy. The initial demo may use immediate delivery at -the sender's current global tick, with sequence ordering defining visibility. A later library may -add fixed, state-dependent, or topology-dependent latency without changing vihaco core. - -## Driver Flow - -`TimelineDriver` repeatedly coordinates the machine: - -```text -read the earliest global event - -> advance GlobalClock.now to that event - -> identify the target CPU or completion - -> obtain an owned runtime instruction or completion - -> call the relevant machine step or handler - -> return scheduling requests to GlobalClock - -> repeat until both programs finish or the machine deadlocks -``` - -The driver may be a reusable library item. The core architecture only requires the one-instruction -`Step` boundary and an owned result that communicates completion, parking, terminal control, and -driver-facing scheduling work. - -Each CPU needs its own program and cursor. For this demo, keeping them in the CPU makes the program -counter modeled child state and demonstrates hardware-owned progression. The driver obtains the -next owned instruction through an explicit top-level operation, allowing any borrow of child -program storage to end before the whole machine is mutably stepped. - -The demo should also distinguish normal completion from deadlock. If both CPUs are parked, no -delivery can satisfy either continuation, and the global event queue is empty, the driver returns a -deadlock result rather than waiting indefinitely. - -## Demonstration Program - -A small deterministic exchange can exercise arithmetic, communication, suspension, and unequal -clock ratios. With the rightmost value treated as the top of each stack: - -```text -CpuA initial stack: [2, 2, 3] -CpuB initial stack: [10, 4] -``` - -The conceptual SST programs are: - -```text -CpuA: - add - send to_b - recv from_b - mul - -CpuB: - sub - recv from_a - mul - send to_a -``` - -The expected value flow is: - -```text -CpuA: 2 + 3 = 5 -CpuA sends 5 to CpuB -CpuB: 10 - 4 = 6 -CpuB receives 5 -CpuB: 6 × 5 = 30 -CpuB sends 30 to CpuA -CpuA receives 30 -CpuA: 2 × 30 = 60 -``` - -With every instruction costing one local cycle and immediate communication delivery, one expected -global trace is: - -```text -global 0: CpuA add; next eligible at 2 -global 0: CpuB sub; next eligible at 3 -global 2: CpuA send 5; CpuA next eligible at 4 -global 3: CpuB recv 5; CpuB next eligible at 6 -global 4: CpuA recv parks -global 6: CpuB mul -> 30; CpuB next eligible at 9 -global 9: CpuB send 30; CpuA receive is satisfied -global 11: CpuA becomes eligible and mul -> 60 -``` - -The exact trace depends on the selected communication timing contract, but the contract and expected -trace must be fixed before the end-to-end test is written. The final observable result for this -configuration is `60` on `CpuA`'s stack, with both programs completed and no parked continuation -left behind. - -## Requirements on the Instruction Rewrite - -The architecture mapped in [`contents.md`](./contents.md) must provide or prove the following -surface area for the demo: - -1. A CPU composite can select only `add`, `sub`, `mul`, `send`, and `recv` from larger reusable - component catalogs. -2. Two instances of the same CPU composite retain distinct machine-local route identities. -3. The top-level composite can address and step either child without exposing all descendant - instructions accidentally. -4. A nested route can propagate an owned effect to its parent, and the parent can route it to a - library-defined handler. -5. A parent can route an owned completion back to the correct child independently of that child's - next program instruction. -6. One effect can reach multiple handlers deterministically, such as a local clock and a diagnostic - trace handler. -7. A step outcome can carry owned driver-facing scheduling work. -8. Parking registers an owned continuation and prevents the driver from scheduling the next - instruction prematurely. -9. Programs and program counters can live in each CPU while the external driver safely obtains an - owned instruction for dispatch. -10. Surface channel names resolve to library-defined runtime identifiers before execution. -11. Generated code preserves typed faults and reports the CPU, route, instruction, global tick, and - failed pipeline stage. - -These requirements constrain the general architecture without making the demo's communication or -clock types part of vihaco core. - -## Reusable Library Deliverables - -The demo should be assembled from reusable items rather than defining all behavior inside the -example: - -- A stack component with invariant-preserving operations. -- Arithmetic runtime instructions and an `i64` arithmetic component implementing `add`, `sub`, and - `mul`. -- Surface instruction types and resolution support for those arithmetic operations. -- A local clock component with a configurable local-cycle-to-global-tick ratio. -- A global clock or event-queue component with deterministic ordering. -- A communication endpoint and shared communication component supplied by a library. -- Surface and runtime `send` and `recv` instructions supplied by that communication library. -- Owned send, receive, delivery, and wakeup effects. -- Program and program-counter components suitable for a child CPU. -- A timeline driver suitable for more than this one machine. - -The final crate and module organization can be decided during implementation. The architectural -requirement is that none of these reusable components relies on the private fields or concrete type -of `HeterogeneousMachine`. - -## Implementation Sequence - -The demo should grow alongside the instruction rewrite: - -1. Build one CPU from a stack and reusable arithmetic unit; execute `add`, `sub`, and `mul` through - generated routes. -2. Instantiate the CPU twice in a parent composite and prove that route identity distinguishes the - two instances. -3. Add local clocks, the global clock, and a timeline driver; verify the two clock ratios with only - arithmetic instructions. -4. Add a library-provided communication component and complete non-parking `send`. -5. Add `recv`, owned continuation registration, parking, delivery, and wakeup. -6. Parse both SST programs, resolve channel names, and load the resulting runtime programs into the - two CPUs. -7. Record and assert the deterministic global trace. -8. Document how to replace the clock or communication library without changing vihaco core. - -Each stage should leave a runnable test. Macro ergonomics can improve after the manual relationships -are proven, but the final demo must use the public composition surface intended for downstream -users. - -## Acceptance Criteria - -The demo is complete when: - -- One top-level composite contains the global clock and two CPU composites. -- Both CPUs use the same reusable component and instruction implementations. -- The top-level composite exposes only the intended child operations. -- `CpuA` maps one local cycle to two global ticks. -- `CpuB` maps one local cycle to three global ticks. -- Global time is monotonic and same-tick ordering is deterministic. -- Arithmetic touches only each CPU's local stack. -- Values cross CPUs only through typed effects and library-defined communication handlers. -- Arithmetic and communication share `i64` directly; no framework value enum or implicit cast is - involved. -- `recv` parks when no value is available and resumes without retaining a borrow. -- A parked CPU does not execute its next instruction. -- Both SST programs lower entirely to the selected runtime instruction sums. -- The expected trace is reproducible. -- `CpuA` finishes with `60` on its stack. -- Both programs terminate with no lost value, stale continuation, or pending event. -- Replacing the communication component does not require a change to vihaco core. -- Building `CpuB` from `CpuA` requires configuration and wiring changes rather than copied execution - implementations. - -The last criterion is central to the demonstration. Heterogeneity should arise from composition, -configuration, timing, and program choice while reusable semantic components remain unchanged. - -## Non-Goals - -The first demo does not need: - -- A general network-on-chip model. -- Dynamic CPU discovery. -- Multiple host threads. -- Wall-clock synchronization. -- Nondeterministic or stochastic timing. -- Backpressure beyond what is required to demonstrate a parked receive. -- A complete debugger or visualization frontend. -- Performance representative of physical hardware. - -Those capabilities may be layered onto the same boundaries later. They are not prerequisites for -showing that vihaco can prototype a heterogeneous machine correctly. diff --git a/vision/design-tradeoffs.md b/vision/design-tradeoffs.md deleted file mode 100644 index 332861a5..00000000 --- a/vision/design-tradeoffs.md +++ /dev/null @@ -1,163 +0,0 @@ -# Design Tradeoffs, Observability, and Errors - -This document records the alternatives behind the selected architecture and the resulting -diagnostic and observability boundaries. - -## Comparison of Alternatives - -The selected component-bound model sits between two simpler designs. Comparing ownership rather -than syntax makes the tradeoff clear. - -### Component-Wide Instruction Enum - -In the component-wide model, state ownership, instruction availability, and dispatch all move -together: - -```text -Component owns: - state + whole instruction enum + whole dispatch - -Composite owns: - collection of components -``` - -Its strengths are: - -- Simple implementation. -- One match performs component dispatch. -- Straightforward single-enum routing. -- Familiar Rust enum ergonomics. - -Its costs appear at composition boundaries: - -- Including a component includes every instruction it supports. -- Unsupported instruction/message combinations may be representable. -- Effects and messages are often coarse enums. -- Component instruction sets are difficult to reuse selectively. -- The machine runtime instruction set is determined accidentally by struct membership. - -### Pure Staged Instructions - -The pure staged model moves all machine state access out of instructions: - -```text -Instruction: - Message -> Result - -Machine: - owns all state resolution and effects -``` - -Its strengths are: - -- Maximum semantic reuse. -- Very easy unit testing. -- Explicit dataflow. -- Strong separation from runtime architecture. -- Excellent observability and simulation potential. - -Its costs appear in stateful machines: - -- Components risk becoming passive storage. -- Local invariant-preserving operations need excessive wiring. -- Simple mutations may require artificial effects. -- The composite carries substantial orchestration code. -- Stateful operations can be awkward or inefficient. - -### Selected Component-Bound Instruction - -The selected component-bound model keeps local state transitions with their owner while making -machine admission and cross-component dataflow explicit: - -```text -Component implements Execute -Composite selects and routes Instruction -``` - -Its strengths are: - -- Selective machine instruction sets. -- Component invariant ownership. -- Typed per-operation messages, effects, and faults. -- Efficient owner-local mutation. -- Explicit cross-component wiring. -- Machine-specific surface parsing and runtime routing. - -Its costs are: - -- Component-bound operations are less portable than pure operations. -- Direct mutations are not automatically visible as effects. -- Duplicate routes require generated route identities. -- Macro and diagnostic complexity increases. -- Cross-component instructions require explicit message/effect staging. -- Tests for stateful instructions need component fixtures. - -This is the default because it places each responsibility at the narrowest stable ownership -boundary. Pure operations remain available through stateless executors, and cross-component -operations deliberately use message and effect staging. - -## Type and Value Ownership - -A framework-owned `Value` enum would make heterogeneous stacks convenient, but it would turn every -machine's guest data model into a vihaco compatibility decision. Component-owned value universes -have the opposite problem: values crossing a stack, arithmetic unit, heap, or channel boundary -would lose one shared identity or require pervasive adapters. - -The selected model leaves semantic values and types with machine and library authors: - -- Vihaco supplies scalar parsing, generic containers, and byte-codec infrastructure. -- Libraries define reusable domain products such as heap or channel identifiers. -- A machine author may define a closed carrier when its architecture requires one. -- Components are generic over, or explicitly support, those products. -- A composite selects concrete compatible instantiations without generating a universal carrier. - -This supports both `Stack` and `Stack` without privileging either architecture. -It also lets multiple composites share one data-model crate. - -Cross-component wiring is exact by default. Automatically casting mismatched message and effect -types would hide whether conversion is checked, saturating, wrapping, lossy, or a bit -reinterpretation. Resolution may insert a conversion defined by the source language, but the -resolved runtime path records that choice explicitly. See -[`types-and-values.md`](./types-and-values.md). - -## Observability and Debugging - -Direct component mutation means not every state change naturally appears in the effect stream. The -architecture does not force artificial command effects solely for observability. The composite -provides step-level hooks, and the driver provides orchestration-level hooks, for: - -- Instruction start and completion. -- Selected route identity. -- Component target. -- Resolved message metadata without exposing sensitive values. -- Emitted effects. -- Execution outcome and faults. -- Modeled start and completion time. - -A component may emit fact events after direct mutation when those events are part of its public -model. Step tracing records route execution; driver tracing records instruction selection, -program-counter changes, parking, wakeups, and modeled time. These hooks remain separate from -semantic effects so enabling diagnostics does not change execution. - -## Error Model - -Failures retain the stage and route in which they occurred. Each `Execute` implementation has a -typed component fault, and the composite converts it into the machine error: - -```rust -MachineFault: From<>::Fault> -``` - -Pattern parsing, module resolution, runtime message resolution, effect handling, and driver -orchestration may also fail. Their diagnostic context identifies: - -- The source instruction and location for parse or module-resolution failures. -- The unresolved label or symbol and the relevant module/function when resolution fails. -- The machine instruction variant. -- The route. -- The target component field. -- The current program position when available. -- The failed stage: parse, module resolve, message resolve, execute, handle, or schedule. - -Conversions preserve the original source chain so machine-level context does not erase the -component or parser failure. diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md index 91b07ae1..61e60e54 100644 --- a/vision/execution-pipeline.md +++ b/vision/execution-pipeline.md @@ -50,7 +50,8 @@ performs source resolution. ## Runtime Execution Pipeline -One-instruction execution starts with a runtime instruction supplied by a driver or direct caller: +One-instruction execution starts with a runtime instruction supplied by a containing runtime root +or direct caller: ```text supplied runtime instruction @@ -58,7 +59,7 @@ supplied runtime instruction -> resolve runtime message -> execute against the route's component -> handle immediate internal effects - -> return the step outcome and any driver-facing work + -> return the step outcome and any root-facing work ``` The composite macro generates one outer `step` dispatcher from the selected runtime routes. Users @@ -107,9 +108,9 @@ representation; further dispatch abstractions are justified only by demonstrated compiler constraints. `step` does not inherently fetch an instruction, iterate a program, define what happens next, or -advance modeled time. A driver-owned program counter is advanced outside `step`. When a program -counter is itself modeled machine state, route handling may mutate that component during `step`, -but that is an explicit machine configuration rather than universal step behavior. +advance modeled time. In the reference runtime, each CPU owns its program counter and route +handling mutates that modeled component during `step`. That is an explicit machine configuration +rather than universal step behavior. ### Stage 1: Message Resolution @@ -164,6 +165,49 @@ Instructions with no live input use `NoMessage`, allowing generation to omit a u resolver. A route's documentation still states whether its nontrivial resolution reads, copies, or consumes machine state. +#### Resolution Sources + +Message resolution has three declaration forms, ordered by how much the composite macro can +generate on the author's behalf: + +- A route with no live input uses `NoMessage`, and generation omits the resolver entirely. +- A route whose entire message comes from one component uses `message from `. The component + supplies the message through a reusable capability, and generation emits a forwarding resolver. +- A route whose message is assembled from several components, or in an order the grammar does not + imply, names a route-local resolver method with `message with `. + +The single-source form is backed by a component capability that is the input dual of effect +absorption: + +```rust +pub trait Supply { + type Fault; + + fn supply(&mut self) -> Result; +} +``` + +A stack implements `Supply>` once, and every `message from ` route reuses it. +The generated resolver is then a forwarding call: + +```rust +fn resolve_integer_add_message( + &mut self, + _instruction: &Add, +) -> Result, MachineFault> { + Ok(self.operand_stack.supply()?) +} +``` + +Resolution is expressed as a route method rather than a route-parameterized trait. Effect handling +earns its trait from two properties that message resolution does not share: component execution +returns an `Effects` stream that a generic drainer folds over, and the generated route marker's +locality permits a component to carry an effect handler directly. A resolved message is instead a +single owned value, and resolution reads across several composite fields, so it can neither be +folded nor relocated onto a component. The reusable part of resolution therefore lives in +`Supply`, while selection between same-typed messages is expressed by distinct resolver methods +rather than by a marker type. + ### Stage 2: Component Execution Component execution applies the resolved operation to its single selected state owner. The call is @@ -288,7 +332,7 @@ machine instruction variant -> target component field -> message resolver -> effect handler - -> route outcome and driver-facing work + -> route outcome and root-facing work ``` The generated machine instruction variant is the canonical route identity during dispatch. The @@ -535,6 +579,66 @@ The generated implementation may use a fully qualified trait call, a private met match-arm body. All three preserve the same public model: the current machine instruction variant selects exactly one effect-handling policy. +##### Component Absorb and Observe Capabilities + +Declarative effect forwarding is backed by reusable component capabilities, so a generated route +handler names a destination without carrying handler behavior. A component that consumes an effect +into its own state implements `Absorb`; a component that only reads an effect implements `Observe`: + +```rust +pub trait Absorb { + type Fault; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; +} + +pub trait Observe { + fn observe(&mut self, effect: &E); +} +``` + +`Absorb` is the effect-side dual of `Supply`. Both are written once per component, are machine +agnostic, and preserve the component's invariants exactly as its ordinary methods do. A generated +`effects to ` handler forwards through `Absorb`, so the composite still owns the routing +decision while the component still owns how its state changes. + +Effect handling then has the same three declaration forms as message resolution: + +- `effects to ` forwards the effect into one component through `Absorb`. +- `effects to , observed by ...` delivers the effect to one consuming component and + any number of read-only observers. +- `effects with ` names a route-local handler for anything the forwarding forms cannot + express. + +Because the generated route marker is a type local to the machine crate, it may also appear in the +trait reference of a handler implemented directly on a component. That locality is what permits a +component to own the effect handler for a specific route without violating the orphan rule, while +two routes that produce the same effect type remain distinct implementations. Handling on the +composite remains the default; component-side handling is available where a component should own its +own effect policy. + +##### Multiple Handlers + +One operation may need to reach more than one component. This resolves into one of three shapes, and +only the composite route decides which applies: + +- Distinct effects. When the destinations need different information, component execution emits one + effect that carries the whole outcome, and the route handler distributes its fields to each + component through their own `Absorb` implementations. The field-to-component mapping is semantic, + so this uses `effects with `. +- Observation. When several components need the same value but only one consumes it, the consuming + component uses `Absorb` and the rest use `Observe`. Observers run before the consumer takes + ownership, so no clone is required. +- Broadcast. When several components must each consume the same value, the effect implements `Clone` + and generation clones it for every destination but the last. Generation admits `effects to a, b` + only when the effect is `Clone`; otherwise it directs the author to observation or an explicit + handler. + +Delivery order is deterministic: observers precede the consumer, and multiple consumers receive the +effect in declaration order. Routing selects a destination from the effect's type and the route, not +from a runtime value. A route that produces heterogeneous outputs carries them in a carrier product +or a sum the handler matches, rather than distributing by inspecting effect contents. + ##### Code-Generation Boundary Code generation supports the ownership model without becoming part of it: @@ -567,14 +671,14 @@ Route provenance matters whenever two identical effect types receive different m - The destination component instance. - Whether a value is pushed, observed, discarded, or transformed. - Whether handling completes immediately or parks the machine. -- Whether a scheduling request remains internal or crosses the driver boundary. +- Whether a scheduling request remains internal or crosses a child-to-root boundary. - Which fault conversion and diagnostic context are attached. - Which handlers receive the effect. Effects therefore do not enter an unlabelled machine-wide queue before route handling. Deferred work retains either equivalent route provenance or an already-resolved continuation. Once route -handling converts the effect into a resource command, diagnostic event, or driver request, ordinary -typed handlers can continue it without the original route marker. +handling converts the effect into a resource command, diagnostic event, or root scheduling request, +ordinary typed handlers can continue it without the original route marker. #### Effect Ordering @@ -602,5 +706,5 @@ for tracing, diagnostic handlers, replay, and future event-sourced runtimes. #### No Effects Owner-local mutation may complete with an empty `Effects`. The route still returns a step -outcome, and the driver can still account for time or select more work. Neither effect production -nor a clock is required by `step`. +outcome, and a containing runtime can still account for time or select more work. Neither effect +production nor a clock is required by `step`. diff --git a/vision/implementation-plan.md b/vision/implementation-plan.md deleted file mode 100644 index 1731d87e..00000000 --- a/vision/implementation-plan.md +++ /dev/null @@ -1,314 +0,0 @@ -# Instruction and Data-Model Rewrite Verification and Migration - -This document turns the architecture into test coverage, migration phases, implementation -questions, and acceptance criteria. - -## Testing Strategy - -Tests follow the same boundaries as the architecture. Narrow tests establish each product and trait -relationship; route and end-to-end tests prove that generation composes them without widening the -machine's public instruction set. - -### Surface Instruction Tests - -Each surface instruction is tested for: - -- Pattern parse round trip for the canonical dialect-qualified form. -- Generated default pattern equivalence where a default is allowed. -- Tuple-index and named-field binding order. -- Nested value/type field parsers. -- Preservation of unresolved names, labels, and symbolic operands. -- Invalid source syntax rejection. - -### Surface Value and Type Tests - -Author-defined value and type products are tested for: - -- Composition from vihaco's scalar and lexical parsers. -- Module parameter and return types using the author-selected surface type. -- Typed literal variants rejecting invalid type/literal pairings where the grammar expresses the - pairing. -- Unresolved literal text preserving the source needed by resolution. -- Out-of-range scalar input returning a parse error without panicking. -- A surface product participating in parsed modules without implementing runtime bytecode traits. - -### Resolution Tests - -Each `Resolve` implementation is tested for: - -- Successful lowering to the expected runtime instruction or instruction sequence. -- Label and symbol replacement with the correct program-image indices. -- Errors for missing, duplicate, or invalid targets. -- Sugar expansion order. -- Machine-specific validation that requires module context. -- Author-defined surface type and literal lowering. -- Explicit source-language conversion insertion. - -The `ConditionalBranch` reference case anchors the boundary: `@foo` survives parsing as a source -label and becomes a fixed-width `InstructionIndex` only during module resolution. - -### Runtime Instruction Tests - -Each runtime instruction is tested for: - -- Construction with fully resolved values. -- Validation of resolved indices and identifiers where applicable. -- Confirmation that no unresolved source-level names remain. - -### Component Execution Tests - -Each `Execute` implementation is tested for: - -- Successful local state transition. -- Fault behavior. -- Message/instruction pairing. -- Emitted effects. -- Documented partial mutation behavior. - -### Composite Route Tests - -Each composite route test establishes that: - -- The surface instruction is present in the machine surface sum. -- The resolved runtime instruction is present in the machine runtime sum. -- The expected field is selected. -- Message data comes from the correct components. -- Effects reach the correct handlers. -- Duplicate instruction types routed to different fields remain distinct. -- Optional route metadata reaches the configured driver. -- Only explicitly selected surface instruction patterns are accepted. -- Prefix-related mnemonics select the correct route regardless of route declaration order. - -### Compile-Fail Tests - -Compile-fail coverage proves that invalid relationships cannot be generated. It rejects: - -- A selected instruction unsupported by its target component. -- Duplicate public variant names. -- Missing message wiring. -- Missing effect handlers. -- Incompatible message or effect types. -- Cross-component value types that differ without an explicit adapter. -- A suspending effect without a continuation-capable handler. -- A selected surface instruction that does not implement `Parse`. -- A machine surface sum with no applicable - `Resolve` implementation. -- Attempting to route a surface instruction directly to component execution. -- Invalid pattern field mappings and unsupported pattern literals. - -### End-to-End Tests - -End-to-end machines cover: - -- A stack-local instruction. -- A pure arithmetic instruction using stack resolution and handling. -- A heap operation spanning stack and heap. -- A control-flow effect. -- An effect handled by both a stateful component and a diagnostic component. -- A parked receive and resumed continuation. -- A sequential driver with a driver-owned cursor. -- A timeline driver coordinating a global clock with child clocks. -- A machine-owned program counter changed by a modeled hardware component. -- A nested composite exposing only selected operations. -- Pattern parsing into a surface instruction, module resolution into a runtime instruction, and - runtime message resolution before execution. -- One machine using a scalar directly without defining a value enum. -- One author-defined heterogeneous value carrier crossing stack, heap, and channel boundaries. - -## Migration Plan - -Migration proceeds from the semantic relationships outward. Manual instruction and execution types -establish the model first; generation follows only after the required relationships are concrete. - -### Phase 1: Establish Surface, Runtime, and Data-Model Boundaries - -1. Establish distinct surface and runtime instruction types. -2. Decide the final names for surface instructions, runtime instructions, and their generated - machine sums. -3. Remove vihaco's built-in guest `Value` and `Type` enums. -4. Provide fallible `Parse` implementations for the supported scalar source forms. -5. Distinguish identifier, symbol, quoted-string, and unresolved-literal helpers. -6. Parameterize parsed function signatures over an author-selected surface type. -7. Keep the surface-instruction marker independent of runtime instruction/bytecode traits. -8. Use the pattern parser generator for all instruction, value, and type surface syntax. -9. Make `Resolve` the explicit lowering boundary. -10. Add a reference branch instruction whose surface form contains labels and whose runtime form - contains resolved `InstructionIndex` values. -11. Test that the generated machine surface sum resolves into a module containing only variants - from the generated runtime sum and author-defined constant/type products. - -### Phase 2: Introduce Per-Instruction Component Execution - -1. Add the `Execute` relationship. -2. Add `NoMessage`, `NoEffect`, and typed fault conventions. -3. Implement several manual examples before designing ergonomic macros. -4. Start with stack-native `Push`, `Drop`, and `Dup`. -5. Add one pure operation such as `Add`. -6. Add one cross-component operation such as `Allocate`. - -### Phase 3: Generate Explicit Composite Routes - -1. Extend or replace `#[composite]` with explicit instruction selection. -2. Generate a machine surface-instruction sum and a machine runtime-instruction sum from the - selected routes. -3. Generate the pattern-based machine parser from only the selected surface instructions. -4. Generate the outer runtime dispatch match. -5. Support the same runtime instruction type routed to multiple fields. -6. Require the resolver's output module to use the selected machine runtime sum. - -### Phase 4: Add Message Resolution and Effect Wiring - -1. Generate `NoMessage` and `NoEffect` defaults only when no explicit policy is present. -2. Add route-local runtime message resolver methods. -3. Add route-local effect handling. -4. Support deterministic delivery of one effect to multiple typed handlers. -5. Define deterministic ordering for multiple and follow-up effects. - -### Phase 5: Add Drivers, Timing, and Suspension - -1. Establish the one-instruction `Step` boundary and its owned outcome. -2. Add a sequential driver with an explicitly owned program cursor. -3. Add `Complete` and `Parked` driver semantics. -4. Add owned continuation registration for `Receive`. -5. Reject borrowed continuation state. -6. Add a timeline driver that owns global time and consumes driver-facing scheduling requests. -7. Demonstrate a child clock as an ordinary component and handler. -8. Demonstrate a machine-owned program counter controlled by a modeled hardware component. -9. Test reset generations and stale completions. - -### Phase 6: Migrate Existing Components - -1. Split each component-wide instruction enum into individual surface and runtime instruction - structs. -2. Group source files by semantic family: stack, arithmetic, heap, control flow, I/O, and runtime - metadata. -3. Give every surface instruction its canonical `#[syntax_class(instruction, head = ...)]` and - `#[pattern = ...]` declarations. -4. Move special field grammars into local value/type syntax types where practical. -5. Represent sugar, interning inputs, labels, and other unresolved operands explicitly in surface - instruction types. -6. Replace old `Value`/`Type` dependencies with scalars, generics, library newtypes, or an - author-defined data model as appropriate. -7. Implement `Resolve` to lower those forms into executable runtime instructions. -8. Move component-local mutations to `Execute` implementations. -9. Move cross-component reads into runtime message resolution. -10. Move cross-component writes and scheduling into effect handling. - -### Phase 7: Remove Automatic Instruction Inheritance - -1. Stop generating one machine variant per component instruction enum. -2. Require explicit route selection for new composites. -3. Deprecate the component-wide `GeneratedComponent::Instruction` association. -4. Remove adapters after downstream code and documentation have migrated. - -### Phase 8: Establish Resolved Bytecode Encoding - -1. Separate surface parsing traits from runtime encoding and decoding traits. -2. Implement portable codecs for supported fixed-width scalars and generic containers. -3. Preserve one global context and the recursive section frame, local header, local payload, child - table, and child-offset structure. -4. Add author-defined codec coverage for one scalar-only section and one heterogeneous data-model - section in the same file. -5. Generate explicit stable route opcodes scoped to each section's machine runtime-instruction sum. -6. Decide whether section schema identities live in fixed framing or author headers, and test - mismatches at the section path that selected the decoder. -7. Encode variable-sized local instruction records with checked lengths and exact payload - consumption. -8. Validate unique expected child names, parent-relative offsets, containment, and non-overlap. -9. Reject `usize`, implicit Rust discriminants, invalid tags, invalid indices, and trailing payload - data at the wire boundary. -10. Prove that recursive SST resolution and bytecode decoding establish equivalent per-section - invariants. - -## Additional Architecture Coverage - -[`demo.md`](./demo.md) is the only end-to-end reference runtime. It exercises nested composites, -heterogeneous clocks, arithmetic reuse, cross-device communication, suspension, and timeline -driving as one coherent machine. - -The demo does not need to contain every operation used to validate the instruction architecture. -The remaining boundaries are better established through focused component tests, route tests, and -small conformance fixtures: - -| Coverage case | Architectural boundary | Test scope | -|---|---|---| -| `Push` and `Drop` | Owner-local stack mutation requires no self-directed effect, while composite selection still controls instruction availability | Component and route tests | -| `Load` | A component-local load may mutate one combined stack/frame owner, while split storage uses message resolution and effect handling | Alternative route fixtures | -| `Allocate` | Values move from a stack to a heap and a reference returns through typed cross-component stages | Focused composite fixture | -| `ConditionalBranch` | SST labels survive parsing, resolve to runtime program indices, and update either a driver-owned or machine-owned program counter | Resolver and control-flow fixture | -| `Call` | Program metadata, call-stack mutation, frame construction, return placement, and program-counter policy remain distinct responsibilities | Focused control-flow fixture | -| `Print` | Value acquisition remains separate from output delivery, and one effect may reach output and diagnostic handlers | Focused handler fixture | -| Simple sequential execution | `step` remains usable without a clock, and a driver-owned cursor can advance a resolved program | Small end-to-end fixture | - -These cases do not need to be assembled into a second general-purpose machine. Their purpose is to -prove individual boundaries that the two-CPU demo does not exercise directly. The sequential -fixture is an implementation milestone and a fast test harness, not another reference runtime. - -## Questions to Revisit After the First Implementation - -Several API choices depend on evidence from the first implementation: - -1. The final names for surface instructions, runtime instructions, and their generated sums. -2. How one-to-many lowering is represented while `Resolve` builds the runtime module. -3. Whether execution should eventually return something other than `Effects`. -4. Whether pure operations use zero-sized executor components or a dedicated adapter. -5. How fact events emitted after direct mutation are distinguished from command effects. -6. Whether canonical dialect heads are always fixed by surface instruction types or may be wrapped - by an explicit machine-local surface instruction type. -7. Which validation belongs in pattern parsing and which belongs in `Resolve`. -8. Whether repeated author data-model parameters justify a common packaging trait. -9. Whether generic tooling eventually requires self-describing type schemas in bytecode. - -Borrow-specific APIs and macro shorthand follow the same rule: they are introduced in response to -concrete compiler friction or repeated boilerplate, not as prerequisites for the architecture. - -These questions do not change the central ownership decision: - -> Components own their state and per-instruction execution; composites own instruction admission, -> route dispatch, cross-component dataflow, and effect routing; drivers own program iteration, -> readiness, scheduling, and modeled time; either a driver or one modeled component owns -> program-counter transitions. Data-model authors own semantic values and types; vihaco supplies -> scalar, staging, composition, and encoding infrastructure. - -## Acceptance Criteria - -The rewrite has established the architecture when all of the following are true: - -- Adding a component field does not automatically add runtime instructions. -- A composite can select two runtime instructions from a component that executes ten. -- A composite can admit a surface form without assuming a one-to-one runtime counterpart. -- The same instruction can be routed to two component instances without trait conflicts. -- An unsupported surface instruction is rejected by the generated pattern parser. -- An individual surface instruction struct can derive its canonical parser with - `#[syntax_class(instruction, head = ...)]` and `#[pattern = ...]`. -- The generated machine parser admits only selected surface instruction patterns. -- Pattern parsing, `Resolve`, runtime message resolution, execution, and effect handling remain - distinct stages. -- Vihaco exports no required guest `Value` or `Type` enum. -- Parsed function signatures use an author-selected surface type. -- A scalar-only machine does not need to define a value enum. -- Author-defined heterogeneous values can cross compatible component boundaries. -- Mismatched boundary types require an explicit conversion instruction, adapter, or handler. -- A surface `ConditionalBranch` can contain `@foo`, while its runtime counterpart contains only a - resolved fixed-width `InstructionIndex`. -- Runtime instructions contain no unresolved source labels, names, or sugar. -- Only runtime instructions are dispatched to components. -- Native stack mutation requires no artificial self-directed effect. -- Arithmetic can be reused without knowing about stack layout. -- Heap allocation can move values across stack and heap through typed stages. -- Effects are routed deterministically and with route provenance. -- A receive instruction can park and resume without retaining borrows. -- The same composite can be run by a simple sequential driver or a timeline driver. -- Calling `step` directly does not require a program, program counter, or clock. -- Program storage and cursor state can have different owners. -- A machine-owned program counter can be advanced by modeled hardware without competing with - driver-owned advancement. -- Driver-facing scheduling requests cross the step boundary as owned state. -- Nested composites expose only their selected public instruction set. -- Compile errors identify the route and missing component/message/effect relationship. -- Existing diagnostic-handler and loader concepts can integrate without becoming the semantic - owner of instruction execution. -- Bytecode round trips author-defined instructions, constants, and types without depending on Rust - layout, variant order, or pointer width. -- One bytecode file can load a root composite and heterogeneous nested sections whose owners use - different instruction, constant, type, header, and opcode schemas. diff --git a/vision/instruction-model.md b/vision/instruction-model.md deleted file mode 100644 index f7992a23..00000000 --- a/vision/instruction-model.md +++ /dev/null @@ -1,569 +0,0 @@ -# Hybrid Component-Bound Instruction Architecture - -## Status and Direction - -Vihaco needs instructions to remain reusable without reducing components to passive storage. A -pure instruction model (i.e. everything is an effect) makes dataflow explicit, but forces even -owner-local state changes through the composite. A component-owned instruction-set model -preserves local invariants, but exposes every instruction carried by every selected component -(i.e. composites must support *every* instruction from each of its composites). - -This architecture takes the useful boundary from each model. Instructions remain individually -selectable types, while components retain responsibility for executing the operations that mutate -their state. - -The heterogeneous two-CPU machine in [`demo.md`](./demo.md) is the integration reference for these -boundaries. The instruction rewrite and demo should develop together: the general -architecture must support the demo without introducing CPU-, clock-, or communication-specific -behavior into vihaco core. - -The model has the following properties: - -- Instructions remain individual Rust structs so that a machine can select them independently. -- Surface instructions describe SST syntax and are parsed exclusively by the pattern parser. -- Runtime instructions contain fully resolved operands and are the only instructions executed by - components. -- Values and type descriptors are supplied by machine and library authors; vihaco core does not - impose a guest `Value` or `Type` enum. -- The machine's `Resolve` implementation lowers surface - instructions and module-level types into runtime products before execution. -- Components remain the owners of state and the invariant-preserving operations over that state. -- A component implements execution for each instruction it supports. -- A composite explicitly selects the instructions that are part of its public instruction set. -- The composite owns machine-level instruction dispatch, message resolution, and effect routing. -- An external driver owns program iteration and any scheduling or modeled time policy needed by - that execution mode. Program-counter transitions have one configured owner: either the driver or - a modeled machine component. -- An instruction may directly mutate the one component selected as its execution target. -- Cross-component inputs and outputs are represented through message resolution and effects. - -The resulting ownership model is: - -| Decision | Owner | -|---|---| -| What syntax is accepted from SST? | Surface instruction types and their patterns | -| What values and types exist? | The selected author-defined data model | -| How are types, labels, symbols, and sugar lowered? | The implementer of `Resolve` | -| What fully resolved data is stored for execution? | Runtime instruction types | -| Which component knows how to execute it? | The selected component's `Execute` implementation | -| Is the instruction available in this machine? | The composite | -| Which component instance receives it? | A composite route | -| Where does non-inline input come from? | Composite message resolution | -| Where do results and effects go? | Composite effect handling | -| Who advances the program counter? | Either the driver or one modeled component, never both | -| How much modeled time passes? | The selected driver, using route, component, or effect data | -| Can execution park or resume? | The driver together with the resource that owns the continuation | - -Components may publish a catalog of operations they can execute, but that catalog is not the -machine's instruction set. The composite selects individual instructions and gives each selection a -machine-local route. - -## Goals - -The architecture is intended to preserve the following properties: - -1. A composite exposes only surface and runtime instructions it explicitly selects. -2. Unsupported surface instructions cannot be parsed, and unsupported runtime instructions cannot - be dispatched by that composite. -3. Each instruction has statically paired message, effect, and fault types for a particular - component implementation. -4. A component can preserve its own invariants without converting every local mutation into an - effect. -5. Cross-component data movement remains explicit in the composite. -6. A reusable semantic instruction can be executed in more than one machine architecture. -7. The same instruction can be routed to multiple instances of the same component type. -8. Synchronous execution remains easy to inline and statically dispatch. -9. Suspension remains limited to instruction boundaries. -10. Nested composites can expose a selected instruction set without leaking all instructions from - their children. -11. The generated surface remains ordinary Rust that could be written manually. -12. Surface instruction products use the checked pattern parser generator. -13. Runtime instruction products never contain unresolved labels or other source-only data. -14. A composite parser is constructed from only the selected surface instructions. -15. `Resolve` is the explicit, type-checked bridge from - parsed surface modules to runtime modules. -16. Components exchange identical author-defined boundary types or use an explicit conversion. - -The ownership and staging of value and type products are defined in -[`types-and-values.md`](./types-and-values.md). - -## Non-Goals - -The first implementation deliberately leaves the following capabilities outside the core model: - -- Roll back state automatically when an instruction faults. -- Permit a borrowed execution context to survive a parked instruction. -- Infer modeled time from how long host execution takes. -- Dynamically discover instructions at runtime. -- Require all component state transitions to be observable effects. -- Make every instruction portable across every machine architecture. -- Decide advanced borrowing or projection ergonomics before the first implementation demonstrates - that they are needed. - -## Instruction Set Shape: Products Selected Into a Sum - -Surface syntax and runtime execution require different representations. They are separate product -types because source-level names are useful during parsing, while execution requires operands that -have already been resolved. - -```rust -use vihaco_parser::Parse; - -// Surface syntax: appears in SST and may contain source-level names. -#[derive(Parse)] -#[syntax_class(instruction, head = "control")] -#[pattern = "'conditional_branch `@` $when_true `,` `@` $when_false"] -pub struct SurfaceConditionalBranch { - pub when_true: String, - pub when_false: String, -} - -// Runtime instruction: stored in the program image and executed. -pub struct ConditionalBranch { - pub when_true: InstructionIndex, - pub when_false: InstructionIndex, -} -``` - -The pattern parser constructs `SurfaceConditionalBranch` from source such as: - -```text -control::conditional_branch @then, @otherwise -``` - -The resolver owns the label table and lowers those names to runtime program indices. The -instruction-specific part can remain a normal helper: - -```rust -impl MyResolver { - fn resolve_conditional_branch( - &mut self, - instruction: SurfaceConditionalBranch, - ) -> eyre::Result { - Ok(ConditionalBranch { - when_true: self.label_index(&instruction.when_true)?, - when_false: self.label_index(&instruction.when_false)?, - }) - } -} -``` - -A composite selects products into two related sums: - -```rust -pub enum MyMachineSurfaceInstruction { - Push(surface::Push), - Add(surface::Add), - ConditionalBranch(surface::ConditionalBranch), -} - -pub enum MyMachineInstruction { - Push(runtime::Push), - Add(runtime::Add), - ConditionalBranch(runtime::ConditionalBranch), -} -``` - -The surface sum is parsed from SST. An implementation of -`Resolve` produces a -`Module` containing runtime instructions for the program image. The -runtime sum is dispatched during execution. - -The mapping is not necessarily one-to-one. One surface instruction may expand into several runtime -instructions, including cases where one source operation selects different execution paths for -different resolved types. A runtime instruction may also be introduced during lowering without a -direct surface form. The invariant is that only runtime instructions reach components. - -A component package may publish surface and runtime instruction catalogs. Those catalogs are not -automatically inherited by a machine; the composite explicitly selects both its accepted surface -syntax and its executable runtime instruction set. The resolver defines the mapping between the two -selected sets rather than requiring every surface operation to name exactly one runtime operation. - -## Core Trait Shape - -A runtime instruction identifies a fully resolved operation. The fact that a particular component -can execute that operation is a separate relationship. Keeping those facts separate allows one -instruction type to participate in several component implementations without giving the -instruction global knowledge of machine state. - -The `Instruction` trait is a marker for runtime operations. Execution behavior belongs to -`Execute`: - -```rust -pub trait Instruction { - // Surface parsing is a separate type-level concern. -} - -pub trait Execute -where - I: Instruction, -{ - type Message: Message; - type Effect: Effect; - type Fault; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result, Self::Fault>; -} -``` - -The essential relationship is: - -```text -Component implements Execute -``` - -It replaces a component-wide associated instruction set: - -```text -Component has one associated InstructionSet enum -``` - -Each supported operation receives its own implementation: - -```rust -pub struct Stack { - values: Vec, -} - -pub struct Push { - pub value: V, -} - -pub struct Drop; - -impl Execute> for Stack { - type Message = NoMessage; - type Effect = NoEffect; - type Fault = StackFault; - - fn execute( - &mut self, - instruction: &Push, - _message: NoMessage, - ) -> Result, StackFault> { - self.push(instruction.value.clone())?; - Ok(Effects::none()) - } -} - -impl Execute for Stack { - type Message = NoMessage; - type Effect = NoEffect; - type Fault = StackFault; - - fn execute( - &mut self, - _instruction: &Drop, - _message: NoMessage, - ) -> Result, StackFault> { - self.pop()?; - Ok(Effects::none()) - } -} -``` - -The syntax remains illustrative. `NoEffect`, for example, may be uninhabited because -`Effects` never needs to construct a value. - -### Why `Execute for Component` - -Placing execution on `Execute for Component` keeps state ownership visible in the type system: - -- The component is visibly responsible for maintaining its invariants. -- An instruction does not need one globally fixed `Component` associated type. -- The same instruction can have implementations for more than one component type. -- Associated message, effect, and fault types may depend on both the instruction and component. -- A stateless or pure instruction can use a zero-sized executor component. -- Tests can replace a component with a small alternative implementation when useful. - -An `Instruction` trait with `execute(&self, &mut C, ...)` can express the same call -mechanically, but it places component behavior on the instruction side and encourages broad -generic state bounds. The public model instead states the ownership relationship directly: -components execute operations. - -### Instruction Identity and Route Identity - -Instruction identity describes an operation, but not its complete path through a machine. A -composite may route the same instruction type to two fields: - -```rust -pub enum MachineInstruction { - PushOperand(stack::Push), - PushCall(stack::Push), -} -``` - -Here and below, `MachineValue` is an illustrative author-defined carrier rather than a vihaco core -type. Both variants contain the same instruction type and may target the same -`Stack` component type, but they target different instances and may have different -message and effect policies. - -The outer variant is therefore part of the route identity. The composite uses it to determine: - -- Target field selection. -- Message resolution. -- Effect handling. -- Optional metadata made available to a driver. -- Tracing and diagnostics. -- Machine-local instruction metadata. - -The generated dispatch must preserve that distinction. Whether it does so with direct match arms or -private marker types remains an internal choice; route identity itself is part of the architecture. - -## Shape of Surface and Runtime Instructions - -A surface instruction preserves the information written in SST: - -```rust -#[derive(vihaco_parser::Parse)] -#[syntax_class(instruction, head = "control")] -#[pattern = "'branch `@` $target"] -pub struct SurfaceBranch { - pub target: String, -} - -#[derive(vihaco_parser::Parse)] -#[syntax_class(instruction, head = "control")] -#[pattern = "'call $arity `,` `@` $target"] -pub struct SurfaceCall { - pub arity: u32, - pub target: String, -} -``` - -A runtime instruction contains the resolved information required by execution: - -```rust -pub struct Branch { - pub target: InstructionIndex, -} - -pub struct Call { - pub arity: u32, - pub target: InstructionIndex, -} -``` - -Surface instruction types therefore: - -- Derive `vihaco_parser::Parse`. -- Own their pattern and dialect head. -- May contain labels, symbolic names, literals, and other source-level values. -- Are inputs to `Resolve`. -- Are never executed by components. -- Are not stored in the runtime program image. - -Runtime instruction types: - -- Contain no unresolved source symbols. -- Implement the runtime instruction marker. -- Are stored in the program image. -- Are the types accepted by `Execute`. -- Need not implement `Parse`. - -Neither representation carries runtime ownership or orchestration state: - -- A reference to its component. -- A reference to the composite. -- A clock or scheduler. -- An event queue. -- A waker. -- A borrowed execution context. -- Runtime scheduler state. - -Runtime instructions may contain resolved semantic configuration such as: - -- An arithmetic type. -- A local index. -- A resolved program index. -- A channel identifier. -- An immediate value. -- An operation mode. - -Information that depends on live machine state belongs in the runtime message rather than either -instruction representation. - -The types of those fields come from the instruction or data-model author. A runtime instruction -may contain `i64`, `ChannelId`, an author-defined runtime type descriptor, or another resolved -product; it does not depend on a framework `Value` or `Type` enum. Surface products similarly use -the author's value and type parsers. Module-level function signatures receive their surface type -as a separate parsed-module parameter, as described in -[`types-and-values.md`](./types-and-values.md). - -## Shape of a Component - -A component owns one coherent domain of state and the operations that preserve that domain's -invariants. Its responsibilities are to: - -- Store one coherent domain of state. -- Expose invariant-preserving domain methods. -- Implement `Execute` for the individual runtime instructions it supports. -- Implement reset, loading, observation, or resource interfaces when those responsibilities - actually belong to the component. -- Avoid exposing its internal fields merely so generated code can mutate them. - -It does not: - -- Define one enum containing all supported instructions. -- Expose one dispatch method matching every instruction. -- Contribute all its instructions to any composite that contains it. -- Know which machine-local route name a composite assigns to an instruction. -- Know which other components receive its effects. -- Know the runtime's clock or scheduling policy. - -The component's public catalog describes which runtime operations have implementations for its -type. The composite separately decides which surface operations are accepted, how they resolve, -which runtime operations exist in the machine, and which component instance receives each one. - -### Components That Are Also Resources - -Stacks, heaps, channels, and clocks may serve both as instruction targets and as resources used by -message resolution or effect handling. Their ordinary Rust methods remain the -invariant-preserving boundary in both roles: - -```rust -impl Stack { - pub fn push(&mut self, value: V) -> Result<(), StackFault> { - // Preserve capacity, frame, and ownership invariants here. - } - - pub fn pop(&mut self) -> Result { - // Preserve underflow and frame-boundary invariants here. - } -} -``` - -Calling these methods from composite wiring does not expose `Push` or `Pop` as program -instructions. Program visibility changes only when the composite selects a route into its machine -instruction sum. - -## Shape of a Composite - -A composite is the architectural junction between reusable component behavior and one concrete -machine. It is: - -- The product of its component fields. -- The authority that selects its instruction sum. -- The owner of machine-level routing. -- The boundary for cross-component data movement. -- The place where one-instruction route policy becomes concrete. - -Program iteration, program-counter advancement, modeled time, and selection of the next runnable -machine are separate concerns. The driver owns iteration, readiness, and modeled time. Cursor -advancement belongs either to the driver or to an explicitly modeled component; it does not become -an implicit composite responsibility merely because the composite owns instruction dispatch. - -For example: - -```rust -pub struct MyMachine { - operand_stack: Stack, - call_stack: Stack, - arithmetic: ArithmeticUnit, - heap: Heap, - channels: Channels, - program: Executor, - clock: ChildClock, -} -``` - -Merely placing these fields in the struct does not add surface or runtime instructions. The -composite declares its accepted surface instructions and executable runtime routes separately: - -```rust -machine! { - composite MyMachine { - operand_stack: Stack, - call_stack: Stack, - arithmetic: ArithmeticUnit, - heap: Heap, - channels: Channels, - program: Executor, - clock: ChildClock, - } - - surface_instructions { - Push => stack::surface::Push; - Add => arithmetic::surface::Add; - Allocate => heap::surface::Allocate; - ConditionalBranch => control_flow::surface::ConditionalBranch; - Send => channel::surface::Send; - } - - runtime_instructions { - Push => stack::runtime::Push on operand_stack; - - Add => arithmetic::runtime::Add on arithmetic { - message from operand_stack; - effects to operand_stack; - } - - Allocate => heap::runtime::Allocate on heap { - message from operand_stack; - effects to operand_stack; - } - - ConditionalBranch => control_flow::runtime::ConditionalBranch on program { - effects to program; - } - - Send => channel::runtime::Send on channels { - message from operand_stack; - effects to clock; - } - } -} -``` - -The syntax is illustrative; the architecture requires the following properties: - -- Each surface instruction is explicitly admitted to SST parsing. -- The machine's `Resolve` implementation may lower each surface instruction to one or more of the - selected runtime instructions. -- Each runtime instruction has a stable machine-local name. -- Each runtime instruction selects exactly one primary execution target. -- Message and effect wiring is route-specific. - -### Generated Surface and Runtime Sums - -From those declarations, the composite produces one sum for each instruction level: - -```rust -pub enum MyMachineSurfaceInstruction { - Push(stack::surface::Push), - Add(arithmetic::surface::Add), - Allocate(heap::surface::Allocate), - ConditionalBranch(control_flow::surface::ConditionalBranch), - Send(channel::surface::Send), -} - -pub enum MyMachineInstruction { - Push(stack::runtime::Push), - Add(arithmetic::runtime::Add), - Allocate(heap::runtime::Allocate), - ConditionalBranch(control_flow::runtime::ConditionalBranch), - Send(channel::runtime::Send), -} -``` - -The pattern parser generator builds the parser for `MyMachineSurfaceInstruction` from the selected -surface patterns. The resolver builds a module containing `MyMachineInstruction` values. - -The surface sum defines what the parser accepts. The runtime sum defines what `step` can dispatch. -Neither sum inherits unselected instructions from component catalogs. - -### Nested Composites - -A nested composite behaves like a component at its parent's boundary while retaining its own -instruction-selection boundary. It exports only the surface and runtime operations that it has -chosen to make public. The parent may: - -- Route a nested instruction set as a whole when that is intentional. -- Select explicit public operations exported by the child. -- Treat the child as a resource or effect handler without exposing its runtime instructions. - -Containment never implies recursive instruction inheritance. diff --git a/vision/macro-generation.md b/vision/macro-generation.md index 892db073..0500a83f 100644 --- a/vision/macro-generation.md +++ b/vision/macro-generation.md @@ -57,6 +57,12 @@ The composite/machine macro: - Collects explicitly selected surface instructions and runtime instruction routes as separate sets. +- Supports both executable composites and structural composites. A composite may be top-level, + nested, or both executable and top-level. +- Treats a local program/instruction stream as optional. When a composite declares `#[program]` and + executable routes, generation includes its runtime instruction sum, fetch/step boundary, and + program-counter completion plumbing. Without `#[program]`, generation does not invent a local + instruction stream. - Rejects duplicate public variant names. - Verifies that each target field implements `Execute`. - Generates the surface instruction sum and its pattern parser. @@ -68,33 +74,182 @@ The composite/machine macro: - Generates the outer execution match. - Generates or calls route-specific message resolvers. - Generates or calls route-specific effect handlers. +- Generates route-specific effect fanout directly in each `execute_generated` instruction arm; a + separate runtime `drain` function is not required. +- Requires each effect route to declare its observers and exactly one handler explicitly. The + declaration is shaped as `effects { observe foo, bar; to foobar; }`. +- Generates observer calls in declaration order with a shared borrow of each effect, then gives + the owned effect to the single handler. +- Type-checks every listed observer against `Observe` and the handler against + `Handle`. +- Converts observer and handler errors into the route error through `Into` (or an + equivalent framework conversion bound), allowing observers to use either the route error or + their own error type. - Applies machine fault conversions. - Attaches optional route metadata that a configured driver may consume. - Preserves component and source-symbol metadata needed by loaders. +### Proposed `machine!` surface + +`machine!` is the author-facing shorthand for a composite declaration. It lowers to the composite +attribute, the runtime-instruction declaration, and the generated execution relationship. The +same surface covers a top-level executable machine such as Cursa and a structural runtime root +such as `HeterogeneousMachine`. + +An executable top-level machine can own a program and child devices: + +```rust +machine! { + composite Cursa { + #[program] + loader: ProgramImage, + + #[device(0x01, alias = "cpu")] + cpu: Cpu, + + #[device(0x02, alias = "fpga")] + fpga: Fpga, + } + + runtime_instructions { + device Cpu => cpu::Instruction { + message with resolve_cpu; + effects with continue_cpu; + } + + device Fpga => fpga::Instruction { + message with resolve_fpga; + effects with continue_fpga; + } + } +} +``` + +The generated portion is equivalent in shape to: + +```rust +#[composite] +#[runtime_instructions( + Cpu => cpu::Instruction { + message with resolve_cpu; + effects with continue_cpu; + }, + Fpga => fpga::Instruction { + message with resolve_fpga; + effects with continue_fpga; + }, +)] +struct Cursa { + #[program] + loader: ProgramImage, + #[device(0x01, alias = "cpu")] + cpu: Cpu, + #[device(0x02, alias = "fpga")] + fpga: Fpga, +} +``` + +The attribute form is the procedural-macro expansion target; authors normally write the +`machine!` form. The macro generates the outer instruction enum, route dispatch, message resolver +calls, effect continuation, and program-counter transitions. The named resolver and handler +methods remain ordinary author code because they contain machine-specific semantics. + +A structural top-level runtime can use the same declaration without a local program: + +```rust +machine! { + composite HeterogeneousMachine { + clock: GlobalClock, + fabric: ChannelFabric, + + #[device(0x01, alias = "cpu_a")] + cpu_a: Cpu, + + #[device(0x02, alias = "cpu_b")] + cpu_b: Cpu, + } + + runtime_instructions {} +} +``` + +This still gets composite metadata and child-section wiring, but its event loop is supplied by +the runtime root rather than generated as a local instruction stepper. A top-level composite may +also emit effects. In that case `effects to parent` is not valid unless the root has an explicit +outer sink; use a host/runtime boundary or a root handler instead. + +## Message Wiring + +Message wiring supports: + +- A `NoMessage` route with no generated resolver. +- A single-component route whose message is supplied through a component `Supply` capability. +- A route-local resolver method for a message assembled from several components. + +Wiring remains type-checked, and a resolved message is an owned value so that a parked route retains +no borrow into the composite. Message resolution is generated as a route method rather than a +marker-parameterized trait, because a message is a single value read across composite fields and +cannot be relocated onto a component. + ## Effect Wiring Effect wiring supports: -- One effect sent to one handler. -- One effect sent through a deterministic chain. -- One effect broadcast to multiple handlers. +- One effect observed by zero or more explicitly listed observers. +- One effect consumed by exactly one explicitly listed handler. - A route-local handler method. - A default handler when the effect is `NoEffect`. +The preferred route syntax is: + +```rust +effects { + observe debug, stdout; + to stack; +} +``` + +The generated body is equivalent in shape to: + +```rust +for effect in effects { + observer_a.observe(&effect).map_err(Into::::into)?; + observer_b.observe(&effect).map_err(Into::::into)?; + handler.handle(effect).map_err(Into::::into)?; +} +``` + +This is a many-readers/one-consumer boundary: observers never consume or clone the effect, and the +handler receives ownership exactly once. `Observe` is route-parameterized so the +same effect type may be observed differently on different routes. A generic observer such as +`DebugTrace` may implement the trait for every `E`/`R` pair satisfying the route bounds. + +The macro cannot discover observer implementations by inspecting the crate. Observer names must +therefore be explicit in each runtime route; the compiler validates that each named field +implements the required observer trait. The macro also preserves observer declaration order. + Wiring remains type-checked. Macro input may contain strings for field names or source aliases, but generated execution never performs string-based runtime routing. Wiring also never inserts a value conversion. The producer and handler types must match, or the route must name an author-defined converter or handler with explicit semantics. +### Optional debug instrumentation + +Future composite debug instrumentation may be opt-in, for example with `#[vihaco::debug]`. It may +inject a private `DebugTrace` field, add that field as an observer to every effect route, and expose +an accessor for the collected records. The generated trace requires observed effects to implement +`Debug` and records the route identity with `std::any::type_name::()`. Nested-composite +scope and per-route opt-out behavior remain design decisions. + ## Bytecode Generation Future bytecode derives and composite generation: - Implement portable primitive codecs in vihaco core. - Compose codecs implemented by the authors of runtime instruction, constant, and type products. -- Encode each component or composite's local section with its own header, payload, and data model. +- Encode each admitted section with its owner's header, payload, and data model; a structural root + may have an empty local instruction payload. - Assign explicit stable section-local route opcodes to each generated machine runtime sum. - Recursively forward child section encoding and loading through named loadable fields. - Preserve the file-wide global context and parent-relative child section table. diff --git a/vision/runtime-drivers.md b/vision/runtime-drivers.md deleted file mode 100644 index 45725678..00000000 --- a/vision/runtime-drivers.md +++ /dev/null @@ -1,372 +0,0 @@ -# Execution Outcomes and Runtime Drivers - -This document defines the boundary between one-instruction execution and the policies that select, -schedule, park, and resume work. - -## Execution Outcomes, Suspension, and Time - -After immediate effect handling, the route reaches a one-instruction execution state: - -```rust -pub enum Execution { - Complete, - Parked, -} -``` - -This enum is the minimal status. A machine with a driver-owned program counter or external -scheduler uses a richer step outcome carrying owned control-flow and scheduling requests beside the -status. Faults remain errors unless a runtime specifically models traps as first-class state. - -`Complete` means: - -- The instruction and all immediate effect handling finished. -- The machine has reached a boundary at which its driver may select more work. -- It does not select the next instruction or imply that another instruction executes at the same - modeled time. - -`Parked` means: - -- The handler atomically registered the work needed to resume. -- No borrow from instruction execution is needed to resume. -- The driver must not treat this execution context as runnable until the corresponding owned - continuation becomes ready. - -Instruction execution remains synchronous. `send`, `recv`, external I/O, and delayed hardware -completion express suspension through effects handled after `execute` returns. - -Timing remains driver policy. The same instruction may have different modeled duration under -different drivers or configurations. Timing data may come from: - -- Driver configuration. -- Optional route metadata. -- Runtime instruction data. -- A component result. -- Resource state. -- A configured timing table. -- A child clock. -- An external completion event. - -Host execution time never determines modeled duration. - -## Runtime and Program Drivers - -A composite can execute one supplied runtime instruction, but that ability does not make it a -running system. Something must still obtain the next instruction from the configured source, -decide when it is eligible to run, interpret the result of the step, and repeat or stop. That -orchestration role is the **driver**. The source may itself be a modeled sequencer, so the driver -need not be the authority that computes the program counter. - -This document uses **runtime** for the top-level running arrangement: a composite machine, a -selected driver policy, and any resolved programs used by that policy. This is distinct from a -*runtime instruction*, which is one fully resolved operation in a program. - -### The Step/Driver Boundary - -The stable machine boundary is one instruction: - -```text -driver obtains a runtime instruction from the configured source - -> machine.step(instruction) - -> resolve runtime message - -> execute on the selected component - -> handle immediate effects - -> return an owned outcome - -> driver interprets the outcome - -> driver selects the next work, waits, or stops -``` - -The driver is necessary because none of the following has one correct policy for every vihaco -machine: - -- Whether instructions come from a stored program, an interactive caller, a device stream, or an - event queue. -- Whether successful completion advances a cursor. -- Whether a branch mutates a machine component or returns a control request to the caller. -- Whether another instruction runs immediately or at a later modeled time. -- Whether one machine runs to completion or several machines are interleaved. -- Whether a parked operation blocks the caller, yields to another machine, or is exposed as an - incomplete result. -- Whether breakpoints, tracing, deterministic replay, or external hardware completions participate - in instruction selection. - -`step` must therefore remain usable without a program counter or clock. A unit test, debugger, or -host application can construct a runtime instruction and call `step` directly. A program driver -builds repetition on top of exactly the same operation. - -Conceptually, the boundary may be expressed as: - -```rust -pub trait Step { - type Instruction; - type Outcome; - type Fault; - - fn step( - &mut self, - instruction: &Self::Instruction, - ) -> Result; -} -``` - -The associated outcome is intentionally machine-specific. A simple machine may need only -`Complete` and `Parked`; a control-flow machine may also need to communicate advance, jump, halt, -trap, or breakpoint information. The framework should not force every machine to carry control -states that it cannot produce. - -The generated dispatch is still valuable even though every match arm has the same three stages. -The runtime instruction variant selects different concrete instruction types, target fields, -message resolvers, effect handlers, and fault conversions. The driver repeats `step`; it does not -replace that route-specific dispatch. - -### Runtime Ownership - -A convenient top-level owner places the driver and machine beside one another: - -```rust -pub trait Driver { - type Output; - - fn run(&mut self, machine: &mut M) -> eyre::Result; -} - -pub struct Runtime { - pub machine: M, - pub driver: D, -} - -impl Runtime { - pub fn run(&mut self) -> eyre::Result - where - D: Driver, - { - self.driver.run(&mut self.machine) - } -} -``` - -The exact API may differ, and the first implementation may use inherent `run` methods instead of a -common `Driver` trait. The important ownership rule is that the driver is external to the -composite it drives. “External” here means that it is not a field that must borrow its containing -composite; it may still be a normal vihaco type in the same process and may be owned by a -`Runtime`. - -This sibling arrangement lets the driver hold its own mutable policy state while borrowing the -whole machine for a step. Placing the driver inside the machine would require mutably borrowing the -driver field and the containing machine at the same time whenever the driver calls `step`. It would -also make a particular execution policy part of the machine's hardware shape. - -The driver should interact with machine state through explicit machine operations. It may inspect a -program counter, fetch through a program-storage interface, drain driver-facing requests, or reset -the machine when those operations are part of the selected design. It should not depend on the -private layout of arbitrary component fields. - -### What a Driver Holds - -A driver owns the state required by its selection and progression policy. Depending on the driver, -that may include: - -- A resolved program or a reference to program storage. -- One program cursor, several cursors, or no cursor. -- Entry-point and halt state. -- Breakpoints, single-step mode, and debugger bookkeeping. -- A runnable set, event queue, modeled current time, and deterministic tie-breaking order. -- Pending external operations and the owned identifiers used to resume them. -- Reset generations used to reject stale completions. -- Replay input, recorded decisions, or a source of test instructions. - -A driver does not own component invariants or execute component operations itself. It supplies a -runtime instruction to the composite and responds to the resulting outcome. Component-local state -remains in components, and cross-component effects remain routed by the composite. - -The normal lifecycle is: - -1. Parse SST and resolve it into a runtime program. -2. Load or attach that program according to the chosen storage model. -3. Reset the machine and driver state as required. -4. Select an entry point or initial event. -5. Select a runtime instruction and call `step`. -6. Interpret completion, control flow, scheduling requests, parking, or faults. -7. Repeat, wait for a completion, or return a terminal result. - -### Driver Families - -Drivers are policies rather than a second kind of machine. Different use cases should be able to -reuse the same composite: - -| Driver | Typical state | Selection policy | -|---|---|---| -| Sequential interpreter | Program and one cursor | Run the instruction at the cursor, then advance or apply control flow | -| Single-step/debugger | Program, cursor, breakpoints, inspection state | Stop at requested boundaries and expose machine state between steps | -| Timeline/emulation driver | Global time, event queue, runnable machines, one or more cursors | Run the earliest eligible event and schedule its follow-up work | -| Cooperative multi-program driver | Programs, cursors, runnable queue | Interleave several execution contexts according to an explicit policy | -| Externally driven adapter | Pending host or device input | Execute instructions supplied by another process or hardware controller | -| Hardware completion driver | Outstanding operations and completion identifiers | Resume work in response to device completions or interrupts | -| Replay/test driver | Recorded or generated instruction decisions | Reproduce a trace or explore instruction sequences deterministically | - -These can be layered. A debugger may wrap a sequential or timeline driver. A replay facility may -record the choices of another driver. A host adapter may feed instructions to a machine that has no -stored program or program counter at all. - -The initial implementation should begin with concrete drivers needed by reference machines. A -universal driver trait is useful only if those drivers demonstrate a stable shared contract. The -one-instruction `Step` boundary is more fundamental than requiring every orchestration policy to -fit one trait immediately. - -### Drivers and Clocks - -A clock is not intrinsically a framework-wide authority. It may occupy either of two roles: - -1. An ordinary component or handler that owns local clock state, translates device ticks, records - durations, or produces scheduling requests. -2. A driver whose notion of global time determines which instruction or event executes next. - -A sequential interpreter that runs at its caller's pace may have no clock. A machine may contain a -child clock component while being driven sequentially. Conversely, a global emulation clock may be -the timeline driver and hold the event queue, current modeled time, programs, and cursors for -multiple child machines. A driver need not be a clock, and a clock need not be a driver. - -This distinction also defines how effects reach a clock. Internal components continue to handle -effects through the same typed, route-specific handling model as every other destination. An effect -may be sent deterministically to several handlers—for example, a child clock that translates a -device delay and a debug component that records it. Each handler receives the same semantic effect -in declaration order, may mutate its own component, and may emit owned follow-up effects. The -shared input need not be consumed by the first handler, and no separate handling semantics are -required merely because one handler uses the effect only for diagnostics. - -If an effect must influence the external driver, its scheduling meaning must survive the `step` -boundary as owned data. A route can accomplish that in either of two broad ways: - -- Return a driver-facing request as part of the step outcome. -- Record the request in explicitly exposed machine state that the driver drains after the step. - -Returning owned requests makes the boundary clearest, while machine-owned queues may be appropriate -when queueing is itself modeled hardware. The first implementation can choose the simpler -representation without changing the semantic rule: scheduling work intended for an external -driver cannot be consumed exclusively by an internal handler. - -A typical clock hierarchy is: - -```text -instruction emits device scheduling effect - -> route sends it to child clock and diagnostic handlers - -> child clock converts device ticks to a global scheduling request - -> step returns that owned request - -> global clock-driver inserts it into the event queue - -> driver resumes the machine when the event becomes current -``` - -The global clock does not need to see every mutation performed directly by `Execute`. It only -needs the information that affects global ordering, modeled duration, or readiness. When a direct -mutation has such consequences, the instruction result or its route must expose the relevant fact -or scheduling request. Purely component-local changes can remain local. - -Any operation that may park must make that fact visible at the driver boundary. The operation may -first emit an effect that an internal resource handles, but the resulting `Parked` outcome and owned -continuation identity must reach the driver. An instruction must not silently block inside -`Execute` or leave the driver believing that the execution context is still runnable. - -### Program and Program-Counter Placement - -Program storage, a program cursor, and the policy that advances the cursor are separate concepts. -They may be colocated for convenience, but the architecture should not require them to have the -same owner. - -| Placement | Appropriate when | Consequences | -|---|---|---| -| Program and cursor in the driver | Ordinary interpretation, debugging, replay, or several cursors over shared code | The machine receives selected instructions and need not model program storage | -| Program in the driver, cursor in the machine | The program is host-owned but the program counter is visible or mutable hardware state | The driver reads the machine cursor, fetches the instruction, and lets machine policy determine the next cursor | -| Program in the machine, cursor in the driver | Program memory is modeled or device-resident but progression is host-controlled | The driver fetches through an explicit machine operation and owns advance/jump policy | -| Program and cursor in the machine | A sequencer or control-flow unit owns both fetch state and progression | The external driver obtains the next owned instruction through the sequencer and then calls `step` | -| Program supplied externally, no cursor | Interactive execution, streaming control, tests, or a hardware command source | Each instruction is supplied directly and `step` remains fully usable | - -Resolved program contents are usually immutable and may be shared. A cursor is mutable execution -state and there may be several cursors for one program. The rewrite should therefore model program -data and per-execution cursor state as distinct concepts even if it retains a convenient -one-program/one-cursor wrapper. The existing `ProgramImage` shape can remain such a convenience, -but combining a module and one program counter must not make that placement a requirement for all -drivers. - -When the driver owns the cursor, control-flow handling returns driver-facing control such as -advance, jump, call, return, halt, or park. The driver is the sole authority that applies those -changes. It must not increment the cursor before the step and then also apply an advance outcome. - -When the machine owns the cursor, a selected control-flow component or route handler mutates it. -The machine's route policy also applies its ordinary sequential advance when no explicit -control-flow operation replaces it. The driver fetches using the current value and reads the -updated value after the step. In this arrangement the driver must not independently infer that -every completed instruction advances by one. There must be one authority for each cursor -transition. - -Machine-owned cursors are important for hardware-oriented models. A sequencer, branch unit, -interrupt controller, direct-memory-access engine, or external device may drive the program -counter. Treating the cursor as a component permits those modeled hardware operations to mutate it, -while the external driver remains responsible for deciding when the machine is allowed to perform -work. A timeline driver can therefore schedule a sequencer without pretending that the global clock -owns the sequencer's program counter. - -Rust ownership affects the fetch interface when program storage lives inside the same machine that -will be mutably stepped. A driver cannot retain a reference borrowed from the machine's program -field while also borrowing the whole machine mutably for `step`. The selected API must end the -fetch borrow before execution—for example, by returning an owned runtime instruction—or separate -immutable program storage from the mutable composite. This is a concrete ownership constraint, not -a reason to prescribe one placement for all machines. - -### Parking and Resumption - -Parking divides ownership between the machine and driver: - -- The component or resource owns the continuation data required to finish the operation. -- The composite ensures that effect handling registers that continuation atomically. -- The step outcome tells the driver that the execution context is no longer runnable. -- The driver owns when the context re-enters its runnable set. -- A completion event carries an owned identity that can be checked against resets or cancellation. - -No borrow from resolution, execution, or effect handling may survive the step. A simple sequential -driver that does not wait for asynchronous work may return `Parked` to its caller. A timeline driver -may keep running other machines until the relevant event is ready. A hardware driver may wait for -an external completion and then resume the registered continuation. These are different driver -policies over the same machine boundary. - -### Consequences for the Rewrite - -The rewrite should establish these pieces in order: - -1. Generate a one-runtime-instruction `step` operation for each composite. -2. Make its outcome sufficient for a caller to distinguish completion, parking, terminal control, - and driver-facing work required by the reference machine. -3. Implement a simple sequential driver without requiring a clock. -4. Keep program data and cursor state conceptually separate, with an initial convenient ownership - arrangement. -5. Add a timeline driver in which the global clock owns selection and scheduling policy. -6. Demonstrate a machine-owned program counter so hardware-driven progression does not become an - afterthought. -7. Generalize a common driver trait only after these concrete drivers reveal the shared API. - -This division keeps component execution reusable while allowing each runtime to decide what -“next,” “now,” and “runnable” mean. - -## Atomicity and Faults - -Atomicity means that another instruction from the same machine does not interleave with the current -step. It does not imply rollback. Every step reaches one of three boundaries: - -- Complete. -- Parked with a registered continuation. -- Faulted. - -Message resolution may consume operands before execution, and the selected component may mutate -itself before returning a fault. A terminal fault may therefore leave partially consumed or -mutated state. - -Avoiding automatic rollback prevents the common path from cloning values solely to recover from a -fault. An operation that requires transactional semantics implements them explicitly in its owning -component or resource. - -Each route documents the relevant failure boundary: - -- Whether operands are read or consumed. -- Which mutations may occur before a fault. -- Whether effect handling itself can fault. -- Whether a parked operation is cancellable. -- What happens to stale completions after reset. diff --git a/vision/sst-resolution.md b/vision/sst-resolution.md index 9f8e7680..b973087b 100644 --- a/vision/sst-resolution.md +++ b/vision/sst-resolution.md @@ -107,7 +107,7 @@ to the same runtime operation. ## Composite Parser Generation -The machine surface parser is the sum of exactly the selected surface products: +An executable composite's surface parser is the sum of exactly the selected surface products: ```text Parse @@ -124,6 +124,11 @@ The pattern parser composes the selected alternatives, including overlapping mne large instruction sets. The composite supplies types and does not implement a separate parsing algorithm. +The non-executing `HeterogeneousMachine` runtime root selects no local surface or runtime +instructions and therefore has no local instruction parser. Its two CPU children parse and resolve +their own programs. A future structural root section may describe child sections and wiring without +creating an empty executable instruction sum. + ## Parsing Versus Resolution Pattern parsing and module resolution are consecutive but distinct boundaries. Parsing always diff --git a/vision/stack-machine-policy.md b/vision/stack-machine-policy.md index 789684ec..199a6697 100644 --- a/vision/stack-machine-policy.md +++ b/vision/stack-machine-policy.md @@ -118,11 +118,14 @@ program-counter placement determines their destination: - With a machine-owned program counter, a route handler applies control effects to the selected program-counter component. -- With a driver-owned program counter, the route returns equivalent driver-facing control. +- The reference CPUs use this machine-owned arrangement; the root event loop does not independently + advance their cursors. - Call-stack selection, frame construction, and return-value placement remain composite routing concerns because they cross component boundaries. -- Timing and selection of the next runnable instruction remain driver concerns, although handlers - may produce the information used for those decisions. +- Timing handlers produce owned scheduling information, while the root runtime and `GlobalClock` + select the next runnable child. -This makes control-flow instructions reusable across different program, cursor, frame, and driver -representations without allowing both the machine and driver to advance the same cursor. +This keeps control-flow instructions reusable across different program, cursor, and frame +representations without allowing both the child and root runtime to advance the same cursor. A +future externally owned cursor can be added after a concrete runtime requires it; the initial +architecture does not generalize that placement. diff --git a/vision/traits.md b/vision/traits.md deleted file mode 100644 index a8818037..00000000 --- a/vision/traits.md +++ /dev/null @@ -1,56 +0,0 @@ -# Vihaco Traits - -## Objective - -We need to make vihaco ideas - instructions, messages, message resolution, effects, and effect handlers - -first class Rust traits and provide the supporting scaffolding for moving between each stage in the vihaco -framework. - -## Instructions - -### Introduction - -Currently, instruction sets are defined using a single Rust enum. This works well for vihaco currently, but becomes -limiting when we think about the framework as a) providing composable and reusable components, and b) becoming a compilation -target for composite DSLs: - -1. We lose information about the specific messages an instruction will need and what effect(s) an instruction will - emit; -2. We don't know the state an instruction will need to access or mutate from its execution environment; -3. Instructions are locked into a specific instruction set, and instructions with identical logic will need to be - implemented twice. - -We will introduce a new instruction trait: - -```rust -/// A single instruction. -/// -/// An instruction receives its [`State`] as a type parameter with capabilities -/// declared. Each capability describes some action that the instruction requires -/// from the external environment. -trait Instruction { - /// The information needed by the instruction that it doesn't have inline. - type Message: Message; - - /// The information that exits an instruction and is dispatched to its - /// handler. - type Effect: Effect; - - fn execute( - &self, - ctx: &mut S, - msg: Self::Message - ) -> Result, S::Error>; -} -``` - -This will solve the above problems by: - -1. Requiring that instructions declare their message and effect as associated types; -2. Requiring the instruction to declare the necessary state and capabilities it needs; -3. Making instructions individual structs that can `impl Instruction` while still being grouped - by an instruction set enum for cheap dispatch. - -### Defining an Instruction Set - -We will make use of an `instruction_set!` proc macro diff --git a/vision/vision.md b/vision/vision.md deleted file mode 100644 index 5f0d3947..00000000 --- a/vision/vision.md +++ /dev/null @@ -1,240 +0,0 @@ -## Objective - -As we begin building on top of vihaco and use it as a compilation target: - -1. We should strive to take advantage of Rust's high level features. The goal is to take common vihaco ideas currently supported by macros - messages, effects, instructions, effect observers, etc. - and move them into the type system where possible. This will allow DSL output - -### Composite DSL - -### Instruction Set DSL - -### Dispatch Loop - -- vihaco needs to own - -### Capability Traits - -- include justification for the idea of capability traits with examples - - instruction has two capabilities that perform on a stack; some use cases might use the same - stack, different stack, etc. - - I might want: - ```rust - struct LoadContext<'a> { - get: &'a Stack, - push: &'a mut Stack - } - ``` - - but Rust won't let me use the same stack for get and push because of mutable borrow rules; we - need to allow for same stack, different stack, etc. capability traits let us do that -- abstraction across runtimes, using multiple contexts for the same instruction, - allowing single runtime to impl same trait many times, etc. - -### First Class `vihaco` Traits - -Instructions - -```rust -/// A single instruction. -/// -/// An instruction receives its [`State`] as a type parameter with capabilities -/// declared. Each capability describes some action that the instruction requires -/// from the external environment. -trait Instruction { - /// The information needed by the instruction that it doesn't have inline. - type Message: Message; - - /// The information that exits an instruction and is dispatched to its - /// handler. - type Effect: Effect; - - fn execute( - &self, - ctx: &mut S, - msg: Self::Message - ) -> Result, S::Error>; -} -``` - -Message Resolution - -```rust -trait ResolveMessage: State + Sized -where - I: Instruction, -{ - fn resolve(&mut self, inst: &I) -> Result; -} - -impl ResolveMessage for T -where - T: State + Sized, - I: Instruction, -{ - #[inline(always)] - fn resolve(&mut self, _inst: &I) -> Result { - Ok(NoMessage) - } -} -``` - -Effect Handling -```rust -trait ResolveMessage: State + Sized -where - I: Instruction, -{ - fn resolve(&mut self, inst: &I) -> Result; -} - -impl ResolveMessage for T -where - T: State + Sized, - I: Instruction, -{ - #[inline(always)] - fn resolve(&mut self, _inst: &I) -> Result { - Ok(NoMessage) - } -} -``## Instructions - -We are going to replace the grouping of instruction sets by enums into individual `impl Instruction` -on Rust structs: - -```rust -trait Instruction { - type Message; - type Result; - type Fault; - - fn execute( - &self, - msg: Self::Message - ) -> Result; -} -``` - -We will continue with the idea of an instruction having three stages: - -1. **Message Resolution**: What does this instruction need from its execution information? -2. **Instruction Execution**: How does the instruction execute? -3. **Effect Handling**: What effect does this instruction have on its environment? - -Moving instructions into their own individual instructions allows for vihaco to know, statically, -what information moves in and out of each instruction. If we were to have a single execute -instruction that matches over variants of an enum, we can construct impossible combinations of -messages and instructions. By moving vihaco ideas into Rust's type system, we can statically ensure -that the information an instruction has during its execution is correct *by construction*. - -### Instruction Stepping - -The barebones representation of a single instruction's entire pipeline is modeled below: - -```rust -fn step(instruction: &I, state: &mut S) -> Result -where - I: Instruction, - S: ResolveMessage + Handle, - S::Error: From, -{ - let msg = state.resolve(instruction)?; - let result = instruction.execute(msg)?; - state.handle(result) -} -``` - -This matches exactly with the three stages of an instruction. - -### Message Resolution - -Message resolution for a specific instruction is dictated through a trait that the machine -implements. This way, instructions stay the same across machines, but the way the message -is resolved can vary based on the encompassing runtime. - -```rust -trait ResolveMessage: State + Sized -where - I: Instruction, -{ - fn resolve(&mut self, inst: &I) -> Result; -} -``` - -Not all instructions require instructions, so we provide a blanket implementation for -`ResolveMessage` for `Instruction`: - -```rust -impl ResolveMessage for T -where - T: State + Sized, - I: Instruction, -{ - #[inline(always)] - fn resolve(&mut self, _inst: &I) -> Result { - Ok(NoMessage) - } -} -``` - -Message resolution is provided for instructions that need more information from their runtime -environment before they can execute. Think of a `Print` instruction: - -```rust -struct Print { - string: usize, -} -``` - -The `Print` instruction might require that strings are interned by the loader before program execution -during module resolution, meaning that it only has a `usize` index into a string intern table. - -### Support for Asynchronous Instructions - -We are going to enforce **instruction boundary suspension**, meaning that an instruction can only perform -a suspending operation in tail position. In vihaco, this will come in the form of an effect, as `Instruction` -requires that `execute` is synchronous. This comes as a - -Take a hypothetical `recv` example: - -```rust -struct Receive { - channel: ChannelId, -} - -impl Instruction for Receive { - /* associated types */ - - fn execute( - &self, - msg: Self::Message - ) -> Result { - /* execution body */ - } -} -``` - - -```rust -enum Execution { - Complete, - Parked, -} -``` - -```rust -fn step(instruction: &I, state: &mut S) -> Result -where - I: Instruction, - S: ResolveMessage + Handle, - S::Error: From, -{ - let msg = state.resolve(instruction)?; - let result = instruction.execute(msg)?; - state.handle(result) -} -``` - ---- -` From d05954e119fa038c2cd41dd70ae8f924f5da4c18 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Thu, 6 Aug 2026 14:24:29 -0400 Subject: [PATCH 04/15] Added macros for composite and component, and rewrote the implementations of the components and composites in the demo --- crates/vihaco-cpu/src/component.rs | 51 +- crates/vihaco-doctests/src/lib.rs | 4 - crates/vihaco-runtime-derive/Cargo.toml | 2 +- .../design/component-macro.md | 229 +++++++ .../design/composite_macro.md | 458 ++++++++++++++ .../design/concepts-to-review.md | 180 ++++++ .../src/attr_component.rs | 234 ------- .../src/attr_composite.rs | 592 ------------------ .../vihaco-runtime-derive/src/attr_observe.rs | 482 -------------- crates/vihaco-runtime-derive/src/common.rs | 53 +- crates/vihaco-runtime-derive/src/component.rs | 290 +++++++++ crates/vihaco-runtime-derive/src/composite.rs | 77 +++ .../src/composite/codegen.rs | 229 +++++++ .../src/composite/loadable.rs | 197 ++++++ .../src/composite/metadata.rs | 69 ++ .../src/composite/syntax.rs | 276 ++++++++ .../src/composite/validate.rs | 189 ++++++ .../src/derive_message.rs | 21 - crates/vihaco-runtime-derive/src/lib.rs | 98 ++- crates/vihaco-runtime/src/execute.rs | 42 ++ crates/vihaco-runtime/src/generated.rs | 12 - crates/vihaco-runtime/src/handle.rs | 17 + crates/vihaco-runtime/src/lib.rs | 17 +- crates/vihaco-runtime/src/observe.rs | 7 +- crates/vihaco-runtime/src/supply.rs | 10 + .../vihaco-runtime/tests/runtime_contract.rs | 112 ++++ crates/vihaco-stdlib/src/observer/stdio.rs | 10 +- crates/vihaco/src/lib.rs | 29 +- crates/vihaco/src/macros/mod.rs | 2 +- .../duplicate-device-code.rs | 23 +- .../duplicate-device-code.stderr | 8 +- .../duplicate-loadable-name.rs | 5 +- .../invalid-loadable-name.rs | 5 +- .../invalid-loadable-name.stderr | 4 +- .../loadable-without-device.rs | 5 +- crates/vihaco/tests/component_macro.rs | 65 ++ crates/vihaco/tests/multisection_bytecode.rs | 100 +-- crates/vihaco/tests/rfc0008_observe_macro.rs | 329 ---------- .../tests/runtime_macro_crate_override.rs | 89 ++- demos/examples/demo.rs | 73 ++- demos/examples/demo/src/cpu.rs | 302 +++------ demos/examples/demo/src/driver.rs | 8 +- demos/examples/demo/src/machine.rs | 87 ++- demos/examples/demo/src/surface.rs | 16 +- demos/examples/demo/stdlib/arithmetic.rs | 58 +- demos/examples/demo/stdlib/channel.rs | 136 ++-- demos/examples/demo/stdlib/clock.rs | 53 +- demos/examples/demo/stdlib/debug_trace.rs | 33 +- demos/examples/demo/stdlib/stack.rs | 32 +- demos/examples/demo/vihaco/execute.rs | 34 +- demos/examples/demo/vihaco/handle.rs | 23 +- demos/examples/demo/vihaco/resume.rs | 4 +- demos/examples/demo/vihaco/route.rs | 2 +- demos/examples/demo/vihaco/supply.rs | 7 +- docs/examples/counter.rs | 27 +- docs/examples/observe.rs | 12 +- docs/examples/quickstart.rs | 37 +- docs/src/pages/guide/components.md | 10 +- docs/src/pages/guide/composites.md | 21 +- docs/src/pages/guide/instructions.md | 2 +- docs/src/pages/guide/messages.md | 16 +- docs/src/pages/guide/observers.md | 59 +- .../composite-surface-runtime-declaration.md | 167 +++++ 63 files changed, 3395 insertions(+), 2446 deletions(-) create mode 100644 crates/vihaco-runtime-derive/design/component-macro.md create mode 100644 crates/vihaco-runtime-derive/design/composite_macro.md create mode 100644 crates/vihaco-runtime-derive/design/concepts-to-review.md delete mode 100644 crates/vihaco-runtime-derive/src/attr_component.rs delete mode 100644 crates/vihaco-runtime-derive/src/attr_composite.rs delete mode 100644 crates/vihaco-runtime-derive/src/attr_observe.rs create mode 100644 crates/vihaco-runtime-derive/src/component.rs create mode 100644 crates/vihaco-runtime-derive/src/composite.rs create mode 100644 crates/vihaco-runtime-derive/src/composite/codegen.rs create mode 100644 crates/vihaco-runtime-derive/src/composite/loadable.rs create mode 100644 crates/vihaco-runtime-derive/src/composite/metadata.rs create mode 100644 crates/vihaco-runtime-derive/src/composite/syntax.rs create mode 100644 crates/vihaco-runtime-derive/src/composite/validate.rs delete mode 100644 crates/vihaco-runtime-derive/src/derive_message.rs create mode 100644 crates/vihaco-runtime/src/execute.rs create mode 100644 crates/vihaco-runtime/src/handle.rs create mode 100644 crates/vihaco-runtime/src/supply.rs create mode 100644 crates/vihaco-runtime/tests/runtime_contract.rs create mode 100644 crates/vihaco/tests/component_macro.rs delete mode 100644 crates/vihaco/tests/rfc0008_observe_macro.rs create mode 100644 vision/composite-surface-runtime-declaration.md diff --git a/crates/vihaco-cpu/src/component.rs b/crates/vihaco-cpu/src/component.rs index 74fb4fc3..b640127d 100644 --- a/crates/vihaco-cpu/src/component.rs +++ b/crates/vihaco-cpu/src/component.rs @@ -9,7 +9,7 @@ use crate::data::CPU; use crate::instruction::RuntimeInstruction; use vihaco::Effects; use vihaco::program::{Type, Value}; -use vihaco::{component, frame::Frame, traits::*}; +use vihaco::{Execute, Execution, StepResult, frame::Frame, traits::*}; impl Reset for CPU { fn reset(&mut self) { @@ -76,14 +76,15 @@ impl CPU { } } -#[derive(Debug, Clone, PartialEq, vihaco::Message)] +#[derive(Debug, Clone, PartialEq)] pub enum CPUMessage { None, FunctionInfo { arity: u32, start_address: u32 }, Print(String), } -#[component(instruction = RuntimeInstruction, message = CPUMessage, effect = StepOutcome)] +impl vihaco::Message for CPUMessage {} + impl CPU { fn execute( &mut self, @@ -117,6 +118,23 @@ impl CPU { } } +impl Execute for CPU { + type Message = CPUMessage; + type Effect = StepOutcome; + type Fault = eyre::Report; + + fn execute( + &mut self, + inst: &RuntimeInstruction, + msg: Self::Message, + ) -> eyre::Result> { + Ok(StepResult { + effects: self.execute(inst.clone(), msg)?, + execution: Execution::Complete, + }) + } +} + impl CPU { pub fn op_span(&mut self, file: u32, start: u32, end: u32) -> eyre::Result { self.span = (file, start, end); @@ -299,15 +317,21 @@ impl CPU { #[allow(clippy::items_after_test_module)] mod tests { use super::*; - use vihaco::{ - Effects, GeneratedComponent, frame::Frame, instruction::OpCode, traits::StackMemory, - }; + use vihaco::{Effects, Execute, frame::Frame, instruction::OpCode, traits::StackMemory}; + + fn execute( + cpu: &mut CPU, + instruction: RuntimeInstruction, + message: CPUMessage, + ) -> eyre::Result> { + Execute::execute(cpu, &instruction, message).map(|result| result.effects) + } #[test] fn cpu_generated_component_executes_instruction_without_message() { let mut cpu = CPU::default(); - GeneratedComponent::execute_generated( + execute( &mut cpu, RuntimeInstruction::Const(Value::I64(7)), CPUMessage::None, @@ -566,7 +590,7 @@ mod tests { ret_pc: 0, }); - let outcome = GeneratedComponent::execute_generated( + let outcome = execute( &mut cpu, RuntimeInstruction::Const(Value::I64(99)), CPUMessage::None, @@ -587,7 +611,7 @@ mod tests { ret_pc: 0, }); - let outcome = GeneratedComponent::execute_generated( + let outcome = execute( &mut cpu, RuntimeInstruction::Label, CPUMessage::FunctionInfo { @@ -613,7 +637,7 @@ mod tests { }); cpu.stack_push(Value::I64(42)); - let outcome = GeneratedComponent::execute_generated( + let outcome = execute( &mut cpu, RuntimeInstruction::Print, CPUMessage::Print("hello".into()), @@ -635,12 +659,7 @@ mod tests { }); cpu.stack_push(Value::I64(42)); - let err = GeneratedComponent::execute_generated( - &mut cpu, - RuntimeInstruction::Print, - CPUMessage::None, - ) - .unwrap_err(); + let err = execute(&mut cpu, RuntimeInstruction::Print, CPUMessage::None).unwrap_err(); assert!(err.to_string().contains("Print requires")); } diff --git a/crates/vihaco-doctests/src/lib.rs b/crates/vihaco-doctests/src/lib.rs index 3ae1dd22..a26ad5e6 100644 --- a/crates/vihaco-doctests/src/lib.rs +++ b/crates/vihaco-doctests/src/lib.rs @@ -25,10 +25,6 @@ mod ex_counter { include!("../../../docs/examples/counter.rs"); } -mod ex_observe { - include!("../../../docs/examples/observe.rs"); -} - mod ex_quickstart { include!("../../../docs/examples/quickstart.rs"); diff --git a/crates/vihaco-runtime-derive/Cargo.toml b/crates/vihaco-runtime-derive/Cargo.toml index 4386f52a..e1800a61 100644 --- a/crates/vihaco-runtime-derive/Cargo.toml +++ b/crates/vihaco-runtime-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "vihaco-runtime-derive" edition = "2024" -description = "Runtime procedural macros (#[derive(Message)], #[component], #[composite]/#[derive(Machine)], #[observe]) for the vihaco-runtime crate." +description = "Runtime procedural macros (#[component] and #[composite]) for the vihaco-runtime crate." version.workspace = true license.workspace = true repository.workspace = true diff --git a/crates/vihaco-runtime-derive/design/component-macro.md b/crates/vihaco-runtime-derive/design/component-macro.md new file mode 100644 index 00000000..69bf8ce1 --- /dev/null +++ b/crates/vihaco-runtime-derive/design/component-macro.md @@ -0,0 +1,229 @@ +# `component!` Macro Design + +## Status + +Design plan; implementation is intentionally out of scope. + +## Purpose + +`component!` declares a reusable component and its runtime instruction products. +It gives each instruction its own product type and places those products in a +stable namespace derived from the component name. + +The macro is a declaration and association boundary. It is not the machine +instruction-set boundary. + +## Responsibilities + +The macro should: + +- Declare the component state type. +- Declare owned runtime instruction product types. +- Preserve generic parameters, const generics, lifetimes where supported, and + `where` clauses. +- Support unit, tuple, and named-field instruction products. +- Make runtime product types constructible by composite-generated resolution, + either through public fields or generated public constructors. +- Provide a stable generated namespace, normally snake case derived from the + component type name. + +The macro must not: + +- Define source syntax or parser patterns. +- Resolve labels, strings, types, or other module-wide source information. +- Choose which instructions a machine exposes. +- Generate a component-wide execution dispatch match. +- Require one message, effect, or fault type for every instruction. +- Inspect, generate, or validate `Execute` implementations. +- Assign persistent opcodes or silently derive bytecode codecs. +- Require a component-wide instruction enum as the execution boundary. + +## Proposed input + +The initial declaration shape is: + +```rust +component! { + component GateBeam { + measure_sites: HashMap>, + local_x_tolerance_um: f64, + local_y_tolerance_um: f64, + measure_x_tolerance_um: f64, + measure_y_tolerance_um: f64, + cz_pair_radius_um: f64, + } + + instruction { + TopHatCZ, + GlobalRZ, + GlobalR, + LocalRZ, + LocalR, + DefineMeasureSites, + Measure, + Reset, + } +} +``` + +The declaration contains runtime products only. Surface names and patterns are +declared by a composite or a separate surface-instruction declaration selected +by the composite. + +The syntax should eventually support named and tuple products as well: + +```rust +instruction { + Push(V), + Store { slot: SlotId, value: V }, + Reset, +} +``` + +## Generated shape + +For the GateBeam example, the conceptual expansion is: + +```rust +pub mod gate_beam { + pub struct GateBeam { + measure_sites: HashMap>, + local_x_tolerance_um: f64, + local_y_tolerance_um: f64, + measure_x_tolerance_um: f64, + measure_y_tolerance_um: f64, + cz_pair_radius_um: f64, + } + + pub mod instruction { + pub struct TopHatCZ; + pub struct GlobalRZ; + pub struct GlobalR; + pub struct LocalRZ; + pub struct LocalR; + pub struct DefineMeasureSites; + pub struct Measure; + pub struct Reset; + } +} +``` + +Component state fields remain private by default. Runtime product fields must +be public when generated composite code constructs products directly: + +```rust +pub struct Push { + pub value: V, +} +``` + +The namespace module and instruction products should be public when the +component is intended for use by composites in other crates. User-supplied +visibility should be preserved where the declaration permits it. + +## Execution association + +Execution is always provided explicitly by the component author, per product, +not by `component!` and not through a component-wide enum: + +```rust +impl Execute for gate_beam::GateBeam { + type Message = MeasureMessage; + type Effect = GateEvent; + type Fault = GateBeamFault; + + // execute implementation +} +``` + +Different instructions may therefore have different message, effect, and fault +types. `component!` does not inspect, generate, or validate these implementations. + +## Composite boundary + +The composite owns the machine-specific instruction set: + +```text +component products + -> selected surface instruction sum and parser + -> module-wide surface resolution + -> selected runtime instruction sum + -> route-specific message resolution + -> component execution and effect handling +``` + +For example, the composite may expose only `Measure` and `Reset` from the +GateBeam catalog, assign source patterns such as +`gatebeam::measure`, and lower them into the corresponding runtime products. +The component declaration does not need to know that those products were +selected, renamed, or reached through source-level sugar. + +## Design constraints from complex components + +The implementation must account for these cases: + +1. A component has instructions with different messages, effects, and faults. +2. One runtime product is executed by multiple component types. +3. A composite exposes only a subset of a component's products. +4. Generic and const-generic component/product types are used. +5. Products have unit, tuple, or named payloads. +6. One instruction emits zero, one, or many homogeneous effects. +7. Heterogeneous effects use an explicit effect sum chosen by the author. +8. An instruction parks and later resumes through an owned continuation. +9. A surface instruction expands into several runtime instructions. +10. Large products may make a grouped enum expensive; no implicit boxing should + be introduced. +11. Generated module names may collide with existing user modules. +12. Product names may collide after normalization or with Rust keywords. + +Borrowed runtime products and GAT-based execution are not part of the initial +design. Supporting them would require a runtime-boundary redesign because +parked execution and persistent modules need owned values. + +## Grouped enums + +The component macro should not require a grouped enum such as: + +```rust +pub enum Instruction { + Push(instruction::Push), + Pop(instruction::Pop), +} +``` + +If a grouped enum is useful as an optional catalog or storage representation, +it must not become the `Execute` boundary and must not impose common message, +effect, or fault types. The composite-generated runtime sum is the normal place +for machine-local grouping. + +## Naming and collision policy + +The default namespace is the snake-case form of the component type, such as +`GateBeam` -> `gate_beam`. The implementation should eventually support an +explicit override, for example: + +```rust +component! { + #[module = gatebeam] + component GateBeam { + // ... + } +} +``` + +The macro should reject collisions rather than silently overwrite or merge +user modules. It should also reject duplicate instruction names and generated +identifier collisions. + +## Implementation phases + +1. Parse the component declaration, state fields, instruction products, and + generic parameters. +2. Validate names, duplicate products, visibility, and supported field forms. +3. Generate the public component namespace and runtime product structs. +4. Add compile-fail coverage for malformed declarations and name collisions. +5. Add generic, const-generic, named-field, tuple, and unit-product tests. +6. Integrate the generated products with composite route selection and runtime + instruction sums. +7. Add documentation examples for a simple stack, GateBeam-like operations, + and a component with per-instruction message/effect types. diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md new file mode 100644 index 00000000..055d13d7 --- /dev/null +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -0,0 +1,458 @@ +# `composite!` Macro Design + +## Status + +Phase-one implementation plan. This document records the agreed runtime-only scope for the +author-facing `composite!` macro. Surface parsing, module resolution, bytecode, and scheduling +remain later work. + +## Purpose + +`composite!` declares a composite's fields and the runtime instruction routes that the composite +supports. It generates the machine-local runtime instruction sum and the repetitive dispatch that +connects messages, components, effects, observers, and handlers. + +The composite owns route selection and cross-component policy. Reusable components own their local +invariants and implement execution or reusable capabilities such as `Supply` and `Absorb`. + +The first implementation targets the runtime model demonstrated by the two-CPU example. The +example's root event loop, timing policy, program-counter bookkeeping, and resume flow remain +ordinary author-written Rust. + +## Phase-one goals + +The macro should: + +- declare the composite struct; +- declare an explicit composite error type in the macro input; +- preserve existing `#[device(...)]` and `#[loadable]` metadata and validation; +- generate a public `Instruction` enum for executable composites; +- generate private route marker types and route-specific trait implementations; +- resolve messages using `none`, `from`, or a composite-owned resolver method; +- execute selected component instructions through `Execute`; +- observe effects in declaration order; +- consume each effect through exactly one handler; +- support reusable `Absorb` delegation and composite-owned custom handlers; +- normalize component, observer, and handler errors into the composite error; and +- support structural composites that omit `runtime_instructions` entirely. + +The generated execution boundary is an inherent method: + +```rust +fn execute_generated( + &mut self, + instruction: &Self::Instruction, +) -> Result; +``` + +It is private by default. The generated instruction enum is public so runtime roots and facade +resolution code can construct it. + +## Explicit non-goals + +Phase one does not: + +- generate surface instruction parsers; +- generate or implement `Resolve` for parsed modules; +- generate bytecode codecs; +- fetch instructions or own a program counter; +- generate resume or continuation dispatch; +- generate timing or scheduling policy; +- define a universal public machine-execution trait; or +- add a `ResolveMessage` trait. + +Message resolution is deliberately composite-owned. `Supply` is a reusable component +capability, while a resolver that reads several composite fields or applies machine-specific +ordering belongs in a named method on the composite. + +## Runtime foundation + +`GeneratedComponent` is removed. The runtime API becomes the per-instruction model used by the +demo: + +```rust +pub trait Execute { + type Message; + type Effect; + type Fault; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} + +pub struct StepResult { + pub effects: Effects, + pub execution: Execution, +} + +pub enum Execution { + Complete, + Parked, +} + +pub struct NoMessage; + +pub trait Supply { + type Fault; + + fn supply(&mut self) -> Result; +} + +pub trait Absorb { + type Fault; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; +} + +pub trait Observe { + type Error; + + fn observe(&mut self, effect: &E) -> Result<(), Self::Error>; +} + +pub trait Handle { + type Error; + + fn handle(&mut self, effect: E) -> Result<(), Self::Error>; +} +``` + +The exact module placement and visibility of these traits belongs to the runtime crate, but the +macro must generate paths that work through both the `vihaco` facade and `vihaco-runtime`. + +## Author-facing syntax + +The initial declaration shape is: + +```rust +composite! { + composite Cpu { + error = CpuFault; + + pub operand_stack: Stack, + pub alu: ArithmeticUnit, + pub channel: ChannelEndpoint>, + pub debug: DebugTrace, + pub program: Vec, + pub pc: usize, + } + + runtime_instructions { + IntegerAdd(Add) => alu { + message from operand_stack; + effects { + observe debug; + absorb with operand_stack; + } + } + + Recv(Recv) => channel { + message none; + effects { + observe debug; + handle with handle_receive; + } + } + } +} +``` + +The `runtime_instructions` block is optional. If it is omitted, the composite takes no +instructions: the macro generates the struct and metadata/section wiring, but no instruction enum +and no `execute_generated` method. + +### Composite declaration + +The macro owns the struct declaration. Fields preserve user visibility and ordinary Rust types, +generics, and `where` clauses where supported by the parser and code generator. `error = E` is +required for executable composites and names the error type at the generated dispatch boundary. + +Existing field metadata remains supported: + +```rust +#[device(0x01, alias = "cpu")] +cpu: Cpu, + +#[device(0x02)] +fpga: Fpga, +``` + +`#[loadable]` continues to identify device fields that participate in generated bytecode/SST +section loading. `#[program]` may remain accepted as a marker for later program plumbing, but has +no phase-one execution semantics. + +### Runtime route declaration + +Each route has the form: + +```text +VariantName(PayloadType) => target_field { ... } +``` + +The variant name is explicit and becomes both the public instruction-enum variant and the basis of +the private route marker name. Explicit names are required because the same runtime payload may be +selected by multiple routes. + +The payload type is passed unchanged to `Execute`. The macro does not create a +component-wide instruction enum or insert implicit conversions. + +Each route requires exactly one message clause and one effect handler. It may list zero or more +observers: + +```text +message none; +message from field; +message with resolver_method; + +effects { + observe observer_a, observer_b; + absorb with destination_field; +} +``` + +The handler alternatives are exclusive: + +```text +absorb with field; +handle with composite_method; +``` + +The old `to` spelling is not part of the design and should be rejected as a normal macro syntax +error. + +## Generated route behavior + +For every route, the macro generates a private route marker, route-aware trait implementations, +and a dispatch arm. Conceptually, a route such as: + +```rust +IntegerAdd(Add) => alu { + message from operand_stack; + effects { + observe debug; + absorb with operand_stack; + } +} +``` + +generates behavior equivalent to: + +```rust +let message = Supply::::supply(&mut self.operand_stack) + .map_err(Into::::into)?; +let result = self.alu.execute(instruction, message) + .map_err(Into::::into)?; + +for effect in result.effects { + Observe::::observe( + &mut self.debug, + &effect, + ) + .map_err(Into::::into)?; + + Handle::::handle(self, effect) + .map_err(Into::::into)?; +} + +Ok(result.execution) +``` + +The generated `Handle` implementation on the composite forwards an `absorb with field` +route to the target field's `Absorb` implementation: + +```rust +fn handle(&mut self, effect: E) -> Result<(), FieldFault> { + self.destination_field.absorb(effect) +} +``` + +For `handle with method`, the generated wrapper calls a method implemented by the composite: + +```rust +impl Cpu { + fn handle_receive( + &mut self, + effect: ReceiveEffect, + ) -> Result<(), ReceiveFault> { + // Composite-owned routing and policy. + Ok(()) + } +} +``` + +The method receives only the owned effect. It does not receive the route marker or instruction; +those are dispatch details. The macro converts its error into the declared composite error. + +Observers are named fields and are called in declaration order. They borrow the effect and never +consume or clone it. The single handler receives ownership exactly once. An empty observer list is +valid. + +## Message resolution + +### `message none` + +The generated arm passes `NoMessage` and does not call a resolver. + +### `message from field` + +The generated arm forwards to the named component capability: + +```rust +let message = Supply::::supply(&mut self.field)?; +``` + +The message is owned before component execution returns, so a parked operation does not retain a +borrow into the composite. + +### `message with method` + +The named method is implemented on the composite and receives the instruction payload: + +```rust +impl Cpu { + fn resolve_add_message( + &mut self, + instruction: &Add, + ) -> Result { + // Read and combine composite state. + todo!() + } +} +``` + +The macro calls the method uniformly even when the method does not need the instruction. This +keeps route-specific resolution explicit without introducing a route-parameterized +`ResolveMessage` trait. + +## Structural composites + +A structural composite may contain clocks, fabrics, devices, or other runtime state but omit +`runtime_instructions`: + +```rust +composite! { + composite HeterogeneousMachine { + error = CpuFault; + + clock: GlobalClock, + transport: SharedTransport, + + #[device(0x01, alias = "cpu_a")] + cpu_a: Cpu, + #[device(0x02, alias = "cpu_b")] + cpu_b: Cpu, + } +} +``` + +This generates the composite declaration, device metadata, and existing section wiring only. The +root event loop, event enum, child selection, timing ratios, resume handling, and deadlock policy +remain hand-written by the author. + +## Validation and diagnostics + +The parser/code generator should reject at macro expansion time: + +- non-struct or malformed composite declarations; +- duplicate public route variant names; +- duplicate device codes, source symbols, aliases, or loadable section names; +- invalid loadable names and loadable fields without devices; +- routes with missing or duplicate message clauses; +- routes with missing or multiple effect handlers; +- duplicate observer fields; +- unknown composite fields, observers, targets, or handler methods where statically detectable; +- unsupported `to` syntax; and +- invalid route or generated identifier names. + +Trait and conversion requirements that depend on resolved Rust types should be expressed through +normal compiler errors with useful generated spans where possible. In particular, compilation must +type-check: + +- `Execute` on the selected target field; +- `Supply` for `message from`; +- `Observe` for every observer; +- `Absorb` for `absorb with`; +- the composite method named by `message with` or `handle with`; and +- `Into` for component, resolver, observer, and handler failures. + +Generated route markers, route implementations, and handler wrappers remain private. Authors use +the public instruction enum and the generated execution boundary, not generated route internals. + +## Implementation sequence + +1. Add the runtime contracts and migrate existing runtime tests away from `GeneratedComponent`. +2. Remove `GeneratedComponent` and update facade/runtime re-exports and examples. +3. Define a `syn` input model for the composite declaration, field metadata, route clauses, + message clauses, observers, and the two handler forms. +4. Reuse the existing device/source-symbol/loadable validation in + `attr_composite.rs` where applicable. +5. Generate the composite struct and public instruction enum for executable composites. +6. Generate private route markers and route-specific `Observe`/`Handle` wrappers. +7. Generate message resolution and `execute_generated` match arms. +8. Add focused success tests and trybuild diagnostics for malformed declarations and missing + bounds. +9. Convert the demo's generated-looking `Cpu` section to `composite!` and preserve its manual + resume/timing code. +10. Convert `HeterogeneousMachine` to a structural `composite!` declaration while preserving its + root event loop. +11. Run formatting, clippy, workspace tests, doctests, and SPDX checks. + +## Test strategy + +### Macro tests + +Cover at least: + +- a no-message route; +- `message from` through `Supply`; +- `message with` through a composite method; +- multiple routes sharing an instruction or effect type; +- observers in declaration order; +- `absorb with` delegation; +- `handle with` composite-owned handling; +- heterogeneous route errors normalized into the composite error; +- a structural composite with no runtime instruction block; and +- generic composite and field types where supported. + +### Compile-fail tests + +Pin diagnostics for duplicate routes, duplicate observers, missing clauses, mutually exclusive +handlers, unsupported `to`, unknown fields, missing `Execute`/`Supply`/`Observe`/`Absorb` bounds, +and missing error conversions. Update line-sensitive `.stderr` fixtures when diagnostics change. + +### Demo acceptance + +The migrated demo must retain its existing behavior: + +- `CpuA` parks on receive; +- `CpuB` computes and sends; +- the root schedules the wakeup at the receiver's next local boundary; +- `CpuA` resumes and computes `60`; and +- the deterministic global execution trace remains unchanged. + +The macro owns CPU route dispatch only. The root continues to own event scheduling and resume +coordination. + +## Review candidates + +After migration, review the placeholder [machine_macro.rs] implementation and remove or replace it +once `composite!` is real. The demo's `Resume`, timing, route, message, effect, and execution +contracts are referenced by the vision and are not deletion candidates merely because some are +outside phase-one generation. Any genuinely unused concept should first be marked for review and +only deleted after a separate decision. + +## Later phases + +Future work may add: + +- generated surface instruction sums and parsers; +- `Resolve` integration and module-wide source resolution; +- generated program/fetch/step and completion plumbing; +- generated resume and continuation routes; +- bytecode encoding and loading for generated runtime instruction sums; +- a shared public machine execution trait if multiple roots need one; and +- a `ResolveMessage` abstraction if repeated runtime-root use demonstrates that named methods are + insufficient. diff --git a/crates/vihaco-runtime-derive/design/concepts-to-review.md b/crates/vihaco-runtime-derive/design/concepts-to-review.md new file mode 100644 index 00000000..6e3ea0a7 --- /dev/null +++ b/crates/vihaco-runtime-derive/design/concepts-to-review.md @@ -0,0 +1,180 @@ +# Concepts to Review + +This is a holding list for vihaco concepts that are unused by the current +runtime/demo path, remain only for compatibility, or are described by stale +documentation. Nothing in this file is approved for deletion. Each item needs +a separate decision after the relevant deferred phase or downstream usage has +been audited. + +## Review criteria + +- `phase-one unused` means the concept is not needed by the current runtime + execution pipeline, but may be required by a planned phase. +- `transitional` means a newer API has replaced the concept in the intended + design, but references or compatibility code remain. +- `likely unused` means there is no current framework/demo consumer visible in + this workspace; external users must still be checked before deletion. +- Stale documentation is listed separately from code so it can be corrected + without prematurely removing an API. + +## Runtime and macro APIs + +### `GeneratedMachine` and `CompositeMetadata` + +Status: phase-one unused; retain pending module/source-symbol resolution. + +Evidence: + +- `composite!` still generates `GeneratedMachine` in + `crates/vihaco-runtime-derive/src/composite.rs`. +- The current consumer is the crate-override test in + `crates/vihaco/tests/runtime_macro_crate_override.rs`. +- `CompositeMetadata::validate_source_symbols` and alias lookup support the + future module-loading/source-resolution path, but are not part of runtime + instruction execution today. + +Decision needed: whether metadata generation belongs in the first public +`composite!` API or should be deferred until module/source-symbol resolution is +implemented. Do not delete the trait or metadata types yet. + +### `CompositeMetadata` helper methods + +Status: phase-one unused/low-use. + +The `devices`, `device_by_name`, `source_symbol_aliases`, +`source_symbol_device_code`, and `validate_source_symbols` helpers are defined +in `crates/vihaco-runtime/src/generated.rs`. Only some are used internally or +by tests. Revisit once `vihaco-syntax::Resolve` and module loading consume +machine metadata. + +### `EffectSink` + +Status: likely legacy compatibility; audit before removal. + +`EffectSink` lives in `crates/vihaco-abi/src/traits/event_sink.rs`. The new +composite runtime routes effects through `Absorb`, `Observe`, and route-aware +`Handle`. Its visible current use is primarily facade/API compile coverage. +Audit ABI consumers and external compatibility requirements before removing or +de-emphasizing it. + +### `Observe` follow-up effect stream + +Status: transitional contract requiring a design decision. + +`crates/vihaco-runtime/src/observe.rs` retains an associated `Effect` type and +returns `Effects` for compatibility with the existing +`#[observe]` macro. Generated composite routing currently discards those +follow-up effects. Decide whether observers should remain effect-producing or +whether the public contract should be simplified to observation returning only +`Result<(), Error>`. + +### Demo-local `Route` trait + +Status: likely unused; mark for deletion review after demo cleanup. + +`demos/examples/demo/vihaco/route.rs` defines a route abstraction, but the +current generated composite uses private route marker types directly as the +`Observe`/`Handle` route parameter. No framework code depends on the demo-local +trait. + +### `machine!` placeholder + +Status: deferred design material, not an implemented framework API. + +`demos/examples/demo/vihaco/machine_macro.rs` contains a placeholder/comment for +the former `machine!` direction. The public first-iteration macro is +`composite!`; decide whether the placeholder should be removed or retained as +historical design material once structural composite migration is complete. + +### Historical `demos/src/main.rs` scaffold + +Status: likely whole-file deletion candidate. + +The file contains an empty `main`, a no-op local `machine!`, and obsolete local +concepts such as `NoFault`, `NoEffect`, `Step`, and local `Type`/`Value` models. +It has no apparent role in the current examples or vision. Confirm that no +workspace target or documentation links to it, then remove it if it is only +historical scaffolding. + +### `SchedulerMetadata` and `SharedDeviceMetadata` + +Status: likely unused; audit before removal. + +`crates/vihaco-abi/src/metadata/scheduler.rs` defines and re-exports these +metadata types, but there are no repository consumers, demo uses, or clear +vision references. Check downstream API compatibility before deleting them. + +### `Message` marker trait + +Status: low-use API requiring review. + +The runtime derives and exports the `Message` marker, but the current demo and +execution contracts use concrete message types and `NoMessage` without needing +the marker. Determine whether it is still a meaningful bound for the derive or +whether it is legacy API surface. + +### `expect_exactly_one_effect` + +Status: likely legacy helper; review after documentation migration. + +`crates/vihaco-runtime/src/generated.rs` exports this helper. It is used by +legacy examples/tests but not by the current demo execution pipeline or the +vision execution model, which handles `Effects` as a stream. It may still be a +useful general helper, so deletion should follow a usage and API audit. + +## Stale documentation and package metadata + +### `GeneratedComponent` references + +Status: transitional remnants; clean up documentation and metadata. + +`GeneratedComponent` was removed from the compiled runtime API, but references +remain in places such as: + +- `crates/vihaco-runtime/Cargo.toml` package description; +- `docs/src/pages/guide/composites.md`; +- `docs/src/pages/guide/components.md`; +- `docs/src/pages/guide/messages.md`; +- `docs/src/pages/guide/observers.md`; +- `docs/src/pages/quickstart.astro`; +- `demos/examples/demo-vihaco-concepts.md`. + +These should be migrated to `Execute -> StepResult` and the +`composite!` route model, or explicitly labeled historical/deferred material. + +### Old attribute-macro documentation + +Status: stale transitional documentation. + +Several guide sections still describe `#[component]` and `#[composite]`, while +the current runtime derive exports function-like `component!` and `composite!`. +The affected examples should be updated or clearly marked as historical before +the public API is considered documented. + +### Stale concept/design references + +Status: documentation reconciliation required. + +- `demos/examples/demo-vihaco-concepts.md` contains stale paths and old + `to `/`effects to` syntax; the agreed syntax is `absorb with` and + `handle with`. +- `vision/macro-generation.md` should be reconciled with the selected route + syntax and the actual `Handle` contract. +- `vision/execution-pipeline.md` presents `HandleEffects` as an alternative + design. Decide whether it remains an explicitly rejected alternative or + should be removed to avoid competing public models. +- Remaining `GeneratedComponent` references in README, guide pages, and demo + concept notes should be migrated or labeled historical. + +## Explicitly not review candidates at this time + +The vision and execution-pipeline documents describe the following as active +runtime concepts, and the current implementation/tests use them: `Execute`, +`Execution`, `StepResult`, `NoMessage`, `Supply`, `Absorb`, `Handle`, route-aware +observation, and the component execution/effect pipeline. Their absence from a +particular demo path is not sufficient evidence for deletion. + +Similarly, `Resume`, clock/scheduling, surface/module resolution, bytecode +loading, `Resolve`, `ProgramImage`, and generated scheduling are deferred or +demo-independent capabilities described by the vision. They are not deletion +candidates merely because the first runtime iteration does not exercise them. diff --git a/crates/vihaco-runtime-derive/src/attr_component.rs b/crates/vihaco-runtime-derive/src/attr_component.rs deleted file mode 100644 index 39088d1f..00000000 --- a/crates/vihaco-runtime-derive/src/attr_component.rs +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{ImplItem, ItemImpl, ReturnType, Token, Type}; - -use crate::common::{resolve_root, strip_vihaco_attrs}; - -struct ComponentArgs { - instruction: syn::Type, - message: syn::Type, - outcome: Option, - effect: Option, -} - -impl Parse for ComponentArgs { - fn parse(input: ParseStream<'_>) -> syn::Result { - let mut instruction = None; - let mut message = None; - let mut outcome = None; - let mut effect = None; - - while !input.is_empty() { - let ident: syn::Ident = input.parse()?; - input.parse::()?; - let ty: syn::Type = input.parse()?; - match ident.to_string().as_str() { - "instruction" => instruction = Some(ty), - "message" => message = Some(ty), - "outcome" => outcome = Some(ty), - "effect" => effect = Some(ty), - _ => { - return Err(syn::Error::new_spanned( - ident, - "unsupported component argument", - )); - } - } - if input.peek(Token![,]) { - input.parse::()?; - } - } - - Ok(Self { - instruction: instruction.ok_or_else(|| { - syn::Error::new(proc_macro2::Span::call_site(), "missing instruction = ...") - })?, - message: message.ok_or_else(|| { - syn::Error::new(proc_macro2::Span::call_site(), "missing message = ...") - })?, - outcome, - effect, - }) - } -} - -pub fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = syn::parse_macro_input!(attr as ComponentArgs); - let mut item_impl = syn::parse_macro_input!(item as ItemImpl); - let root = match resolve_root(&item_impl.attrs) { - Ok(root) => root, - Err(err) => return err.into_compile_error().into(), - }; - strip_vihaco_attrs(&mut item_impl.attrs); - let instruction_ty = args.instruction; - let message_ty = args.message; - - let self_ty = &item_impl.self_ty; - let has_execute = item_impl.items.iter().any(|item| match item { - ImplItem::Fn(func) => func.sig.ident == "execute" && func.sig.inputs.len() == 3, - _ => false, - }); - if !has_execute { - return syn::Error::new_spanned( - &item_impl.self_ty, - "expected fn execute(&mut self, inst, msg)", - ) - .into_compile_error() - .into(); - } - - for item in &item_impl.items { - let ImplItem::Fn(func) = item else { - continue; - }; - if func.sig.ident != "execute" || func.sig.inputs.len() != 3 { - continue; - } - - let ReturnType::Type(_, ty) = &func.sig.output else { - return syn::Error::new_spanned( - &func.sig, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Type::Path(type_path) = ty.as_ref() else { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Some(result_segment) = type_path.path.segments.last() else { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if result_segment.ident != "Result" { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let syn::PathArguments::AngleBracketed(result_args) = &result_segment.arguments else { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if result_args.args.is_empty() || result_args.args.len() > 2 { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let success_ty = match result_args.args.first() { - Some(syn::GenericArgument::Type(success_ty)) => success_ty, - _ => { - return syn::Error::new_spanned( - ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - }; - let Type::Path(success_path) = success_ty else { - return syn::Error::new_spanned( - success_ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Some(success_segment) = success_path.path.segments.last() else { - return syn::Error::new_spanned( - success_ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if success_segment.ident != "Effects" { - return syn::Error::new_spanned( - success_ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let syn::PathArguments::AngleBracketed(success_args) = &success_segment.arguments else { - return syn::Error::new_spanned( - success_ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if success_args.args.len() != 1 - || !matches!( - success_args.args.first(), - Some(syn::GenericArgument::Type(_)) - ) - { - return syn::Error::new_spanned( - success_ty, - "component execute handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - } - - if args.outcome.is_some() && args.effect.is_some() { - return syn::Error::new( - proc_macro2::Span::call_site(), - "use either effect = ... or outcome = ..., not both", - ) - .into_compile_error() - .into(); - } - - let effect_ty = args - .effect - .or(args.outcome) - .map(|ty| quote! { #ty }) - .unwrap_or_else(|| quote! { () }); - - // split out generics and where clause - let (impl_generics, _ty_generics, where_clause) = item_impl.generics.split_for_impl(); - - quote! { - #item_impl - - impl #impl_generics #root::GeneratedComponent for #self_ty #where_clause { - type Instruction = #instruction_ty; - type Message = #message_ty; - type Effect = #effect_ty; - - fn execute_generated( - &mut self, - inst: Self::Instruction, - msg: Self::Message, - ) -> ::eyre::Result<#root::Effects> { - self.execute(inst, msg) - } - } - } - .into() -} diff --git a/crates/vihaco-runtime-derive/src/attr_composite.rs b/crates/vihaco-runtime-derive/src/attr_composite.rs deleted file mode 100644 index ad05014f..00000000 --- a/crates/vihaco-runtime-derive/src/attr_composite.rs +++ /dev/null @@ -1,592 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{ToTokens, format_ident, quote}; -use std::collections::{BTreeMap, BTreeSet}; -use syn::parse::{Parse, ParseStream}; -use syn::spanned::Spanned; -use syn::{Data, DeriveInput, Fields, GenericParam, Lifetime, LitStr, Token}; - -use crate::common::resolve_root; - -struct DeviceArgs { - code: u8, - aliases: Vec, -} - -struct SectionLoadArgs { - section_name: Option, -} - -impl Parse for SectionLoadArgs { - fn parse(input: ParseStream) -> syn::Result { - let section_name = if input.is_empty() { - None - } else { - let section_name = input.parse::()?; - if !input.is_empty() { - return Err(input.error("unexpected tokens in loadable attribute")); - } - Some(section_name) - }; - Ok(SectionLoadArgs { section_name }) - } -} - -struct SectionLoadField { - field: syn::Ident, - ty: syn::Type, - section_name: String, -} - -impl Parse for DeviceArgs { - fn parse(input: ParseStream<'_>) -> syn::Result { - let code_lit: syn::LitInt = input.parse()?; - let code = code_lit.base10_parse::()?; - let mut aliases = Vec::new(); - while input.peek(Token![,]) { - input.parse::()?; - if input.is_empty() || !input.peek(syn::Ident) { - break; // trailing comma - } - let ident: syn::Ident = input.parse()?; - input.parse::()?; - match ident.to_string().as_str() { - "alias" => { - aliases.push(input.parse::()?); - } - _ => { - return Err(syn::Error::new_spanned( - ident, - "unsupported device argument", - )); - } - } - } - Ok(Self { code, aliases }) - } -} - -fn pascal_case(ident: &syn::Ident) -> syn::Ident { - let mut out = String::new(); - for part in ident.to_string().split('_') { - if part.is_empty() { - continue; - } - let mut chars = part.chars(); - if let Some(first) = chars.next() { - out.push(first.to_ascii_uppercase()); - out.push_str(chars.as_str()); - } - } - format_ident!("{}", out) -} - -fn validate_loadable_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> { - if name.is_empty() { - return Err(syn::Error::new( - span, - "loadable section name cannot be empty", - )); - } - if name.contains('/') { - return Err(syn::Error::new( - span, - "loadable section name cannot contain `/`", - )); - } - Ok(()) -} - -fn method_where_clause(predicates: &[TokenStream2]) -> TokenStream2 { - if predicates.is_empty() { - quote! {} - } else { - quote! { - where - #( #predicates ),* - } - } -} - -fn stream_contains_ident(stream: TokenStream2, ident: &syn::Ident) -> bool { - stream.into_iter().any(|tree| match tree { - proc_macro2::TokenTree::Ident(found) => found == *ident, - proc_macro2::TokenTree::Group(group) => stream_contains_ident(group.stream(), ident), - _ => false, - }) -} - -fn stream_contains_lifetime(stream: &TokenStream2, lifetime: &Lifetime) -> bool { - stream.to_string().contains(&lifetime.to_string()) -} - -fn generic_param_used(param: &GenericParam, streams: &[TokenStream2]) -> bool { - match param { - GenericParam::Lifetime(param) => streams - .iter() - .any(|stream| stream_contains_lifetime(stream, ¶m.lifetime)), - GenericParam::Type(param) => streams - .iter() - .any(|stream| stream_contains_ident(stream.clone(), ¶m.ident)), - GenericParam::Const(param) => streams - .iter() - .any(|stream| stream_contains_ident(stream.clone(), ¶m.ident)), - } -} - -fn stream_mentions_any_generic(stream: TokenStream2, params: &[GenericParam]) -> bool { - params.iter().any(|param| match param { - GenericParam::Lifetime(param) => stream_contains_lifetime(&stream, ¶m.lifetime), - GenericParam::Type(param) => stream_contains_ident(stream.clone(), ¶m.ident), - GenericParam::Const(param) => stream_contains_ident(stream.clone(), ¶m.ident), - }) -} - -fn enum_generics_for_device_fields( - generics: &syn::Generics, - devices: &[(syn::Ident, syn::Type, DeviceArgs)], -) -> syn::Generics { - let device_streams: Vec<_> = devices - .iter() - .map(|(_, ty, _)| ty.to_token_stream()) - .collect(); - let retained_params: Vec = generics - .params - .iter() - .filter(|param| generic_param_used(param, &device_streams)) - .cloned() - .collect(); - - let mut enum_generics = generics.clone(); - enum_generics.params = retained_params.iter().cloned().collect(); - - if let Some(where_clause) = &mut enum_generics.where_clause { - where_clause.predicates = where_clause - .predicates - .iter() - .filter(|predicate| { - stream_mentions_any_generic(predicate.to_token_stream(), &retained_params) - }) - .cloned() - .collect(); - if where_clause.predicates.is_empty() { - enum_generics.where_clause = None; - } - } - - enum_generics -} - -pub fn expand(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - match try_expand(input) { - Ok(tokens) => tokens.into(), - Err(err) => err.into_compile_error().into(), - } -} - -fn try_expand(input: DeriveInput) -> syn::Result { - let root = resolve_root(&input.attrs)?; - let ident = input.ident; - let generics = input.generics; - let data = match input.data { - Data::Struct(data) => data, - _ => { - return Err(syn::Error::new( - proc_macro2::Span::call_site(), - "composite wiring can only be generated for structs", - )); - } - }; - let fields = match data.fields { - Fields::Named(fields) => fields.named, - _ => { - return Err(syn::Error::new( - proc_macro2::Span::call_site(), - "composite wiring requires a struct with named fields", - )); - } - }; - - let mut devices = Vec::new(); - let mut loadables = Vec::::new(); - - for field in fields { - let field_ident = field.ident.expect("named field"); - let field_ty = field.ty; - let mut is_device = false; - let mut loadable_args = None; - for attr in &field.attrs { - let path = attr.path(); - if path.is_ident("device") { - is_device = true; - let args = attr.parse_args::()?; - devices.push((field_ident.clone(), field_ty.clone(), args)); - } else if path.is_ident("loadable") { - if loadable_args.is_some() { - return Err(syn::Error::new( - attr.span(), - format!("duplicate loadable attribute on field `{}`", field_ident), - )); - } - loadable_args = Some(if matches!(&attr.meta, syn::Meta::Path(_)) { - SectionLoadArgs { section_name: None } - } else { - attr.parse_args::()? - }); - } - } - if let Some(args) = loadable_args { - if !is_device { - return Err(syn::Error::new( - field_ident.span(), - format!( - "field `{}` marked #[loadable] must also be marked #[device(...)]", - field_ident - ), - )); - } - let section_name = if let Some(lit) = args.section_name { - let value = lit.value(); - validate_loadable_name(&value, lit.span())?; - value - } else { - let value = field_ident.to_string(); - validate_loadable_name(&value, field_ident.span())?; - value - }; - loadables.push(SectionLoadField { - field: field_ident.clone(), - ty: field_ty.clone(), - section_name, - }); - } - } - - let mut seen_device_codes = BTreeMap::::new(); - for (field, _, args) in &devices { - if let Some(existing) = seen_device_codes.insert(args.code, field.clone()) { - return Err(syn::Error::new( - field.span(), - format!( - "duplicate device code 0x{:02X} for fields `{}` and `{}`", - args.code, existing, field - ), - )); - } - } - - let mut seen_source_symbols = BTreeMap::::new(); - for (field, _, args) in &devices { - let field_name = field.to_string(); - if let Some(existing) = seen_source_symbols.insert(field_name.clone(), field.clone()) { - return Err(syn::Error::new( - field.span(), - format!( - "duplicate source symbol `{}` for `{}` and `{}`", - field_name, existing, field - ), - )); - } - - let mut local_aliases = BTreeSet::new(); - for alias in &args.aliases { - let alias_name = alias.value(); - if !local_aliases.insert(alias_name.clone()) { - return Err(syn::Error::new( - alias.span(), - format!("duplicate alias `{}` on field `{}`", alias_name, field), - )); - } - if let Some(existing) = seen_source_symbols.insert(alias_name.clone(), field.clone()) { - return Err(syn::Error::new( - alias.span(), - format!( - "duplicate source symbol `{}` for `{}` and `{}`", - alias_name, existing, field - ), - )); - } - } - } - - let mut seen_loadable_names = BTreeMap::::new(); - for loadable in &loadables { - if let Some(existing) = - seen_loadable_names.insert(loadable.section_name.clone(), loadable.field.clone()) - { - return Err(syn::Error::new( - loadable.field.span(), - format!( - "duplicate loadable section name `{}` for fields `{}` and `{}`", - loadable.section_name, existing, loadable.field - ), - )); - } - } - - let machine_instruction_ident = format_ident!("{}Instruction", ident); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let enum_generics = enum_generics_for_device_fields(&generics, &devices); - let (_, enum_ty_generics, _) = enum_generics.split_for_impl(); - - let machine_instruction_variants: Vec<_> = devices - .iter() - .map(|(field, field_ty, _)| { - let variant_ident = pascal_case(field); - quote! { - #variant_ident(<#field_ty as #root::GeneratedComponent>::Instruction) - } - }) - .collect(); - - let device_entries: Vec<_> = devices - .iter() - .map(|(field, _, args)| { - let name = field.to_string(); - let code = args.code; - quote! { #root::metadata::DeviceMetadata { code: #code, name: #name } } - }) - .collect(); - let source_symbol_alias_entries: Vec<_> = devices - .iter() - .flat_map(|(_, _, args)| { - let code = args.code; - let root = root.clone(); - args.aliases.iter().map(move |alias| { - quote! { - #root::metadata::SourceSymbolAliasMetadata { - name: #alias, - device_code: #code, - } - } - }) - }) - .collect(); - - let bc_lifetime = Lifetime::new("'__vihaco_bc", proc_macro2::Span::call_site()); - let loadable_context_param = format_ident!("__VihacoContext"); - let mut loadable_predicates = Vec::::new(); - loadable_predicates.push( - quote! { #ident #ty_generics: #root::loader::LoadOwnBytecodeSection<#loadable_context_param> }, - ); - for loadable in &loadables { - let ty = &loadable.ty; - loadable_predicates - .push(quote! { #ty: #root::loader::LoadBytecodeSection<#loadable_context_param> }); - } - let loadable_method_where = method_where_clause(&loadable_predicates); - - let mut loadable_impl_generics = generics.clone(); - loadable_impl_generics - .params - .push(syn::parse_quote!(#loadable_context_param)); - if !loadable_predicates.is_empty() { - let where_clause = loadable_impl_generics.make_where_clause(); - for predicate in &loadable_predicates { - where_clause - .predicates - .push(syn::parse2(predicate.clone())?); - } - } - let (loadable_impl_generics, _, loadable_where_clause) = - loadable_impl_generics.split_for_impl(); - - let loadable_names: Vec<_> = loadables - .iter() - .map(|loadable| loadable.section_name.clone()) - .collect(); - let child_loads: Vec<_> = loadables - .iter() - .map(|loadable| { - let field = &loadable.field; - let ty = &loadable.ty; - let name = &loadable.section_name; - quote! { - if let ::std::option::Option::Some(__vihaco_child) = section.child(#name) { - <#ty as #root::loader::LoadBytecodeSection<#loadable_context_param>>::load_bytecode_section( - &mut self.#field, - __vihaco_child, - )?; - } - } - }) - .collect(); - - let loadable_impl = quote! { - impl #impl_generics #ident #ty_generics #where_clause { - pub fn load_generated_bytecode_sections<#bc_lifetime, #loadable_context_param>( - &mut self, - section: #root::BytecodeSectionView<#bc_lifetime, #loadable_context_param>, - ) -> ::eyre::Result<()> - #loadable_method_where - { - #root::loader::LoadOwnBytecodeSection::<#loadable_context_param>::load_own_bytecode_section( - self, - section.clone(), - )?; - - let __vihaco_expected_children: &[&str] = &[#(#loadable_names),*]; - - for __vihaco_child in section.children() { - let __vihaco_child_name = __vihaco_child.local_name().ok_or_else(|| { - ::eyre::eyre!( - "section `{}` yielded a root section as a child", - section.display_path(), - ) - })?; - if !__vihaco_expected_children - .iter() - .any(|__vihaco_expected| *__vihaco_expected == __vihaco_child_name) - { - return Err(::eyre::eyre!( - "section `{}` has unexpected child section `{}`", - section.display_path(), - __vihaco_child.display_path(), - )); - } - } - - #( #child_loads )* - Ok(()) - } - } - - impl #loadable_impl_generics #root::loader::LoadBytecodeSection<#loadable_context_param> - for #ident #ty_generics - #loadable_where_clause - { - fn load_bytecode_section<#bc_lifetime>( - &mut self, - section: #root::BytecodeSectionView<#bc_lifetime, #loadable_context_param>, - ) -> ::eyre::Result<()> { - self.load_generated_bytecode_sections(section) - } - } - }; - - let mut text_loadable_predicates = Vec::::new(); - text_loadable_predicates.push( - quote! { #ident #ty_generics: #root::loader::LoadOwnSstSection<#loadable_context_param> }, - ); - for loadable in &loadables { - let ty = &loadable.ty; - text_loadable_predicates - .push(quote! { #ty: #root::loader::LoadSstSection<#loadable_context_param> }); - } - let text_loadable_method_where = method_where_clause(&text_loadable_predicates); - - let mut text_loadable_impl_generics = generics.clone(); - text_loadable_impl_generics - .params - .push(syn::parse_quote!(#loadable_context_param)); - if !text_loadable_predicates.is_empty() { - let where_clause = text_loadable_impl_generics.make_where_clause(); - for predicate in &text_loadable_predicates { - where_clause - .predicates - .push(syn::parse2(predicate.clone())?); - } - } - let (text_loadable_impl_generics, _, text_loadable_where_clause) = - text_loadable_impl_generics.split_for_impl(); - - let text_child_loads: Vec<_> = loadables - .iter() - .map(|loadable| { - let field = &loadable.field; - let ty = &loadable.ty; - let name = &loadable.section_name; - quote! { - if let ::std::option::Option::Some(__vihaco_child) = section.child(#name) { - <#ty as #root::loader::LoadSstSection<#loadable_context_param>>::load_sst_section( - &mut self.#field, - __vihaco_child, - )?; - } - } - }) - .collect(); - - let text_loadable_impl = quote! { - impl #impl_generics #ident #ty_generics #where_clause { - pub fn load_generated_sst_sections<#bc_lifetime, #loadable_context_param>( - &mut self, - section: #root::SstSectionView<#bc_lifetime, #loadable_context_param>, - ) -> ::eyre::Result<()> - #text_loadable_method_where - { - #root::loader::LoadOwnSstSection::<#loadable_context_param>::load_own_sst_section( - self, - section.clone(), - )?; - - let __vihaco_expected_children: &[&str] = &[#(#loadable_names),*]; - - for __vihaco_child in section.children() { - let __vihaco_child_name = __vihaco_child.local_name().ok_or_else(|| { - ::eyre::eyre!( - "section `{}` yielded a root section as a child", - section.display_path(), - ) - })?; - if !__vihaco_expected_children - .iter() - .any(|__vihaco_expected| *__vihaco_expected == __vihaco_child_name) - { - return Err(::eyre::eyre!( - "section `{}` has unexpected child section `{}`", - section.display_path(), - __vihaco_child.display_path(), - )); - } - } - - #( #text_child_loads )* - Ok(()) - } - } - - impl #text_loadable_impl_generics #root::loader::LoadSstSection<#loadable_context_param> - for #ident #ty_generics - #text_loadable_where_clause - { - fn load_sst_section<#bc_lifetime>( - &mut self, - section: #root::SstSectionView<#bc_lifetime, #loadable_context_param>, - ) -> ::eyre::Result<()> { - self.load_generated_sst_sections(section) - } - } - }; - - Ok(quote! { - #[derive(Debug, Clone, #root::Instruction)] - pub enum #machine_instruction_ident #enum_generics { - #( #machine_instruction_variants ),* - } - - impl #impl_generics #root::__private::GeneratedMachine for #ident #ty_generics #where_clause { - type Instruction = #machine_instruction_ident #enum_ty_generics; - - fn metadata(&self) -> #root::CompositeMetadata { - static DEVICES: &[#root::metadata::DeviceMetadata] = &[ - #( #device_entries ),* - ]; - static SOURCE_SYMBOL_ALIASES: &[#root::metadata::SourceSymbolAliasMetadata] = &[ - #( #source_symbol_alias_entries ),* - ]; - #root::CompositeMetadata { - devices: DEVICES, - source_symbol_aliases: SOURCE_SYMBOL_ALIASES, - } - } - } - - #loadable_impl - #text_loadable_impl - }) -} diff --git a/crates/vihaco-runtime-derive/src/attr_observe.rs b/crates/vihaco-runtime-derive/src/attr_observe.rs deleted file mode 100644 index b0446fda..00000000 --- a/crates/vihaco-runtime-derive/src/attr_observe.rs +++ /dev/null @@ -1,482 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use convert_case::{Case, Casing}; -use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::ToTokens; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{ - GenericArgument, ImplItem, ItemImpl, Path, PathArguments, ReturnType, Token, Type, - parse_macro_input, -}; - -use crate::common::{resolve_root, strip_vihaco_attrs}; - -struct ObserveEntry { - event_type: syn::Path, -} - -struct ObserveArgs { - entries: Vec, - composite_effect: Option, -} - -impl Parse for ObserveArgs { - fn parse(input: ParseStream<'_>) -> syn::Result { - let mut entries = Vec::new(); - let mut composite_effect = None; - - while !input.is_empty() { - if input.peek(syn::Ident) { - let lookahead = input.fork(); - let ident: syn::Ident = lookahead.parse()?; - if lookahead.peek(Token![=]) { - if ident == "snapshot" { - return Err(syn::Error::new_spanned( - ident, - "snapshot = ... is no longer supported in #[observe]", - )); - } - if ident != "effect" { - return Err(syn::Error::new_spanned( - ident, - "unsupported #[observe] metadata; expected `effect = ...`", - )); - } - if composite_effect.is_some() { - return Err(syn::Error::new_spanned( - ident, - "duplicate `effect = ...` in #[observe]", - )); - } - - input.parse::()?; - input.parse::()?; - composite_effect = Some(input.parse()?); - } else { - entries.push(ObserveEntry { - event_type: input.parse()?, - }); - } - } else { - entries.push(ObserveEntry { - event_type: input.parse()?, - }); - } - - if input.peek(Token![,]) { - input.parse::()?; - } - } - - if entries.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - "#[observe] requires at least one effect type", - )); - } - - Ok(Self { - entries, - composite_effect, - }) - } -} - -fn types_equivalent(lhs: &Type, rhs: &Type) -> bool { - match (lhs, rhs) { - (Type::Array(lhs), Type::Array(rhs)) => { - expr_tokens_eq(&lhs.len, &rhs.len) && types_equivalent(&lhs.elem, &rhs.elem) - } - (Type::Group(lhs), rhs) => types_equivalent(&lhs.elem, rhs), - (lhs, Type::Group(rhs)) => types_equivalent(lhs, &rhs.elem), - (Type::Paren(lhs), rhs) => types_equivalent(&lhs.elem, rhs), - (lhs, Type::Paren(rhs)) => types_equivalent(lhs, &rhs.elem), - (Type::Path(lhs), Type::Path(rhs)) => paths_equivalent(&lhs.path, &rhs.path), - (Type::Ptr(lhs), Type::Ptr(rhs)) => { - lhs.mutability.is_some() == rhs.mutability.is_some() - && types_equivalent(&lhs.elem, &rhs.elem) - } - (Type::Reference(lhs), Type::Reference(rhs)) => { - lhs.mutability.is_some() == rhs.mutability.is_some() - && lifetimes_equivalent(lhs.lifetime.as_ref(), rhs.lifetime.as_ref()) - && types_equivalent(&lhs.elem, &rhs.elem) - } - (Type::Slice(lhs), Type::Slice(rhs)) => types_equivalent(&lhs.elem, &rhs.elem), - (Type::Tuple(lhs), Type::Tuple(rhs)) => { - lhs.elems.len() == rhs.elems.len() - && lhs - .elems - .iter() - .zip(rhs.elems.iter()) - .all(|(lhs, rhs)| types_equivalent(lhs, rhs)) - } - _ => lhs.to_token_stream().to_string() == rhs.to_token_stream().to_string(), - } -} - -fn paths_equivalent(lhs: &Path, rhs: &Path) -> bool { - lhs.segments.len() == rhs.segments.len() - && lhs - .segments - .iter() - .zip(rhs.segments.iter()) - .all(|(lhs, rhs)| { - lhs.ident == rhs.ident && path_arguments_equivalent(&lhs.arguments, &rhs.arguments) - }) -} - -fn path_arguments_equivalent(lhs: &PathArguments, rhs: &PathArguments) -> bool { - match (lhs, rhs) { - (PathArguments::None, PathArguments::None) => true, - (PathArguments::AngleBracketed(lhs), PathArguments::AngleBracketed(rhs)) => { - lhs.args.len() == rhs.args.len() - && lhs - .args - .iter() - .zip(rhs.args.iter()) - .all(|(lhs, rhs)| generic_arguments_equivalent(lhs, rhs)) - } - (PathArguments::Parenthesized(lhs), PathArguments::Parenthesized(rhs)) => { - lhs.inputs.len() == rhs.inputs.len() - && lhs - .inputs - .iter() - .zip(rhs.inputs.iter()) - .all(|(lhs, rhs)| types_equivalent(lhs, rhs)) - && match (&lhs.output, &rhs.output) { - (ReturnType::Default, ReturnType::Default) => true, - (ReturnType::Type(_, lhs), ReturnType::Type(_, rhs)) => { - types_equivalent(lhs, rhs) - } - _ => false, - } - } - _ => false, - } -} - -fn generic_arguments_equivalent(lhs: &GenericArgument, rhs: &GenericArgument) -> bool { - match (lhs, rhs) { - (GenericArgument::Type(lhs), GenericArgument::Type(rhs)) => types_equivalent(lhs, rhs), - (GenericArgument::Lifetime(lhs), GenericArgument::Lifetime(rhs)) => lhs == rhs, - (GenericArgument::Const(lhs), GenericArgument::Const(rhs)) => expr_tokens_eq(lhs, rhs), - (GenericArgument::AssocType(lhs), GenericArgument::AssocType(rhs)) => { - lhs.ident == rhs.ident && types_equivalent(&lhs.ty, &rhs.ty) - } - (GenericArgument::AssocConst(lhs), GenericArgument::AssocConst(rhs)) => { - lhs.ident == rhs.ident && expr_tokens_eq(&lhs.value, &rhs.value) - } - (GenericArgument::Constraint(lhs), GenericArgument::Constraint(rhs)) => { - lhs.ident == rhs.ident - && lhs.bounds.len() == rhs.bounds.len() - && lhs.bounds.iter().zip(rhs.bounds.iter()).all(|(lhs, rhs)| { - lhs.to_token_stream().to_string() == rhs.to_token_stream().to_string() - }) - } - _ => false, - } -} - -fn lifetimes_equivalent(lhs: Option<&syn::Lifetime>, rhs: Option<&syn::Lifetime>) -> bool { - match (lhs, rhs) { - (Some(lhs), Some(rhs)) => lhs == rhs, - (None, None) => true, - _ => false, - } -} - -fn expr_tokens_eq(lhs: &syn::Expr, rhs: &syn::Expr) -> bool { - lhs.to_token_stream().to_string() == rhs.to_token_stream().to_string() -} - -pub fn expand(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as ObserveArgs); - let mut item_impl = parse_macro_input!(item as ItemImpl); - let root = match resolve_root(&item_impl.attrs) { - Ok(root) => root, - Err(err) => return err.into_compile_error().into(), - }; - strip_vihaco_attrs(&mut item_impl.attrs); - let self_ty = &item_impl.self_ty; - - let mut trait_impls = Vec::new(); - - for entry in &args.entries { - let event_path = &entry.event_type; - - // Get the last segment name for the method naming convention - let event_name = event_path.segments.last().unwrap().ident.to_string(); - - // Convert to snake_case prefix: observe_channel_frame for ChannelFrame - let method_prefix = format!("observe_{}", event_name.to_case(Case::Snake)); - - // Find all methods whose name matches or starts with the prefix - let matching_methods: Vec<&syn::Ident> = item_impl - .items - .iter() - .filter_map(|item| { - if let ImplItem::Fn(f) = item { - let name = f.sig.ident.to_string(); - if name == method_prefix || name.starts_with(&format!("{}_", method_prefix)) { - return Some(&f.sig.ident); - } - } - None - }) - .collect(); - - if matching_methods.is_empty() { - return syn::Error::new_spanned( - event_path, - format!( - "missing handler method `{}` (or `{}_*`) for observed effect `{}`", - method_prefix, method_prefix, event_name - ), - ) - .into_compile_error() - .into(); - } - - let expected_inputs = 2; - let label = "handler"; - let params_desc = "&mut self, effect"; - - let mut follow_up_calls = Vec::new(); - let mut effect_error_ty = None::; - let mut has_non_unit_follow_up = false; - for method_ident in &matching_methods { - if let Some(ImplItem::Fn(f)) = item_impl - .items - .iter() - .find(|item| matches!(item, ImplItem::Fn(f) if &f.sig.ident == *method_ident)) - { - if f.sig.inputs.len() != expected_inputs { - return syn::Error::new_spanned( - &f.sig, - format!( - "{} `{}` must have {} parameters: {}", - label, f.sig.ident, expected_inputs, params_desc - ), - ) - .into_compile_error() - .into(); - } - - let ReturnType::Type(_, ty) = &f.sig.output else { - return syn::Error::new_spanned( - &f.sig, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Type::Path(type_path) = ty.as_ref() else { - return syn::Error::new_spanned( - ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Some(segment) = type_path.path.segments.last() else { - return syn::Error::new_spanned( - ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - - if segment.ident == "Result" { - let syn::PathArguments::AngleBracketed(result_args) = &segment.arguments else { - return syn::Error::new_spanned( - ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if result_args.args.is_empty() || result_args.args.len() > 2 { - return syn::Error::new_spanned( - ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let _inner_ty = match &result_args.args[0] { - syn::GenericArgument::Type(inner_ty) => inner_ty.clone(), - other => { - return syn::Error::new_spanned( - other, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - }; - let Type::Path(success_path) = &_inner_ty else { - return syn::Error::new_spanned( - &_inner_ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - let Some(success_segment) = success_path.path.segments.last() else { - return syn::Error::new_spanned( - &_inner_ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if success_segment.ident != "Effects" { - return syn::Error::new_spanned( - &_inner_ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let syn::PathArguments::AngleBracketed(success_args) = - &success_segment.arguments - else { - return syn::Error::new_spanned( - &_inner_ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - }; - if success_args.args.len() != 1 - || !matches!( - success_args.args.first(), - Some(syn::GenericArgument::Type(_)) - ) - { - return syn::Error::new_spanned( - &_inner_ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - let local_effect_ty = match success_args.args.first() { - Some(syn::GenericArgument::Type(local_effect_ty)) => { - local_effect_ty.clone() - } - _ => unreachable!(), - }; - let is_unit_follow_up = - matches!(&local_effect_ty, Type::Tuple(tuple) if tuple.elems.is_empty()); - if !is_unit_follow_up { - has_non_unit_follow_up = true; - } - let error_ty = if result_args.args.len() == 1 { - syn::parse_quote!(::eyre::Report) - } else { - match &result_args.args[1] { - syn::GenericArgument::Type(error_ty) => error_ty.clone(), - other => { - return syn::Error::new_spanned( - other, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - } - }; - if let Some(existing_error_ty) = &effect_error_ty { - if !types_equivalent(existing_error_ty, &error_ty) { - return syn::Error::new_spanned( - &f.sig.output, - format!( - "observer handlers for `{}` must use the same error type; expected `{}`", - event_name, - existing_error_ty.to_token_stream() - ), - ) - .into_compile_error() - .into(); - } - } else { - effect_error_ty = Some(error_ty.clone()); - } - let follow_up_call = if let Some(composite_effect) = &args.composite_effect { - if is_unit_follow_up { - quote! { - let __follow_ups: #root::Effects<#local_effect_ty> = - ::std::convert::Into::<#root::Effects<_>>::into(self.#method_ident(effect)?); - for () in __follow_ups {} - } - } else { - quote! { - effects = effects.extend( - ::std::convert::Into::<#root::Effects<_>>::into(self.#method_ident(effect)?) - .map(::std::convert::Into::<#composite_effect>::into) - ); - } - } - } else { - quote! { - let __follow_ups: #root::Effects<#local_effect_ty> = - ::std::convert::Into::<#root::Effects<_>>::into(self.#method_ident(effect)?); - for () in __follow_ups {} - } - }; - follow_up_calls.push(follow_up_call); - } else { - return syn::Error::new_spanned( - ty, - "observer handlers must return Result, Error>", - ) - .into_compile_error() - .into(); - } - } - } - - let error_ty = - effect_error_ty.unwrap_or_else(|| syn::parse_quote!(::std::convert::Infallible)); - let is_composite_generated = - args.entries.len() > 1 || matching_methods.len() > 1 || has_non_unit_follow_up; - if is_composite_generated && args.composite_effect.is_none() { - return syn::Error::new_spanned( - event_path, - "generated #[observe] impls that compose multiple observed events, multiple handlers, or typed follow-up effects must declare `effect = ...` in #[observe(...)]", - ) - .into_compile_error() - .into(); - } - let generated_effect_ty = args - .composite_effect - .clone() - .unwrap_or_else(|| syn::parse_quote!(())); - trait_impls.push(quote! { - impl #root::Observe<#event_path> for #self_ty { - type Effect = #generated_effect_ty; - type Error = #error_ty; - - fn observe( - &mut self, - effect: &#event_path, - ) -> ::std::result::Result<#root::Effects, Self::Error> { - let mut effects = #root::Effects::none(); - #( #follow_up_calls )* - Ok(effects) - } - } - }); - } - - quote! { - #item_impl - #( #trait_impls )* - } - .into() -} diff --git a/crates/vihaco-runtime-derive/src/common.rs b/crates/vihaco-runtime-derive/src/common.rs index 3649b8da..0b7497fd 100644 --- a/crates/vihaco-runtime-derive/src/common.rs +++ b/crates/vihaco-runtime-derive/src/common.rs @@ -4,7 +4,58 @@ use proc_macro_crate::{FoundCrate, crate_name}; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; -use syn::Ident; +use syn::parse::ParseStream; +use syn::punctuated::Punctuated; +use syn::{Field, GenericParam, Generics, Ident, Token}; + +/// Parse a comma-separated sequence of named fields. +pub fn parse_named_fields(input: ParseStream<'_>) -> syn::Result> { + Punctuated::parse_terminated_with(input, Field::parse_named) +} + +/// Retain only the generic parameters referenced by the supplied token streams. +pub fn retain_generics(generics: &Generics, references: &[TokenStream2]) -> Generics { + let mut result = generics.clone(); + result.params = generics + .params + .iter() + .filter(|param| { + references + .iter() + .any(|tokens| generic_param_is_referenced(param, tokens)) + }) + .cloned() + .collect(); + + if let Some(where_clause) = &mut result.where_clause { + where_clause.predicates = where_clause + .predicates + .iter() + .filter(|predicate| { + let tokens = quote!(#predicate); + result + .params + .iter() + .any(|param| generic_param_is_referenced(param, &tokens)) + }) + .cloned() + .collect(); + if where_clause.predicates.is_empty() { + result.where_clause = None; + } + } + result +} + +fn generic_param_is_referenced(param: &GenericParam, tokens: &TokenStream2) -> bool { + // TODO: improve: inspect the syntax tree instead of matching token strings. + let tokens = tokens.to_string(); + match param { + GenericParam::Type(param) => tokens.contains(¶m.ident.to_string()), + GenericParam::Const(param) => tokens.contains(¶m.ident.to_string()), + GenericParam::Lifetime(param) => tokens.contains(¶m.lifetime.to_string()), + } +} /// Remove attributes consumed by this proc-macro crate before re-emitting an /// item. Attribute macros cannot register helper attributes, so leaving one on diff --git a/crates/vihaco-runtime-derive/src/component.rs b/crates/vihaco-runtime-derive/src/component.rs new file mode 100644 index 00000000..efbd9d04 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/component.rs @@ -0,0 +1,290 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use crate::common::{parse_named_fields, retain_generics}; +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::{format_ident, quote}; +use std::collections::BTreeMap; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Field, Fields, Generics, Ident, Result, Token, Visibility, WhereClause}; + +syn::custom_keyword!(component); +syn::custom_keyword!(instruction); + +struct ComponentDeclaration { + module: Option, + visibility: Visibility, + name: Ident, + generics: Generics, + state: Fields, + products: Vec, +} + +struct InstructionProduct { + attrs: Vec, + visibility: Visibility, + name: Ident, + fields: Fields, +} + +impl Parse for ComponentDeclaration { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = Attribute::parse_outer(input)?; + let module = parse_module_attribute(&attrs)?; + let visibility = input.parse()?; + input.parse::()?; + let name = input.parse()?; + let mut generics: Generics = input.parse()?; + generics.where_clause = input.parse::>()?; + let content; + syn::braced!(content in input); + let state = parse_fields(&content)?; + + let products = if input.peek(instruction) { + input.parse::()?; + let content; + syn::braced!(content in input); + syn::punctuated::Punctuated::::parse_terminated( + &content, + )? + .into_iter() + .collect() + } else { + Vec::new() + }; + + if !input.is_empty() { + return Err(input.error("unexpected tokens after component declaration")); + } + + Ok(Self { + module, + visibility, + name, + generics, + state, + products, + }) + } +} + +impl Parse for InstructionProduct { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = Attribute::parse_outer(input)?; + let visibility = input.parse()?; + let name = input.parse()?; + let fields = if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + Fields::Unnamed(syn::FieldsUnnamed { + paren_token: Default::default(), + unnamed: syn::punctuated::Punctuated::::parse_terminated_with( + &content, + Field::parse_unnamed, + )?, + }) + } else if input.peek(syn::token::Brace) { + let content; + syn::braced!(content in input); + Fields::Named(syn::FieldsNamed { + brace_token: Default::default(), + named: syn::punctuated::Punctuated::::parse_terminated_with( + &content, + Field::parse_named, + )?, + }) + } else { + Fields::Unit + }; + + Ok(Self { + attrs, + visibility, + name, + fields, + }) + } +} + +fn parse_fields(input: ParseStream<'_>) -> Result { + let fields = parse_named_fields(input)?; + Ok(Fields::Named(syn::FieldsNamed { + brace_token: Default::default(), + named: fields, + })) +} + +fn parse_module_attribute(attrs: &[Attribute]) -> Result> { + let mut module = None; + for attr in attrs { + if !attr.path().is_ident("module") { + return Err(syn::Error::new_spanned( + attr, + "unsupported component attribute; expected `#[module = name]`", + )); + } + let value = match &attr.meta { + syn::Meta::NameValue(value) => match &value.value { + syn::Expr::Path(path) if path.path.segments.len() == 1 => { + path.path.segments[0].ident.clone() + } + _ => { + return Err(syn::Error::new_spanned( + &value.value, + "module name must be an identifier", + )); + } + }, + _ => return Err(syn::Error::new_spanned(attr, "expected `#[module = name]`")), + }; + if module.replace(value).is_some() { + return Err(syn::Error::new_spanned(attr, "duplicate module attribute")); + } + } + Ok(module) +} + +fn product_generics(generics: &Generics, fields: &Fields) -> Generics { + retain_generics(generics, &[quote! { #fields }]) +} + +fn validate(declaration: &ComponentDeclaration, module: &Ident) -> Result<()> { + validate_generated_name(&module.to_string(), module.span())?; + let mut names = BTreeMap::new(); + for product in &declaration.products { + let normalized = module_name(&product.name).to_case(Case::Snake); + validate_generated_name(&normalized, product.name.span())?; + if let Some(previous) = names.insert(normalized, product.name.clone()) { + return Err(syn::Error::new( + product.name.span(), + format!("instruction name collides with `{previous}` after normalization"), + )); + } + } + Ok(()) +} + +fn validate_generated_name(name: &str, span: Span) -> Result<()> { + syn::parse_str::(name) + .map(|_| ()) + .map_err(|_| syn::Error::new(span, "generated name is not a valid Rust identifier")) +} + +fn module_name(ident: &Ident) -> String { + ident.to_string().trim_start_matches("r#").to_owned() +} + +fn public_fields(fields: Fields) -> Fields { + match fields { + Fields::Named(mut fields) => { + for field in &mut fields.named { + if matches!(field.vis, Visibility::Inherited) { + field.vis = syn::parse_quote!(pub); + } + } + Fields::Named(fields) + } + Fields::Unnamed(mut fields) => { + for field in &mut fields.unnamed { + if matches!(field.vis, Visibility::Inherited) { + field.vis = syn::parse_quote!(pub); + } + } + Fields::Unnamed(fields) + } + Fields::Unit => Fields::Unit, + } +} + +fn parent_visible_fields(mut fields: Fields) -> Fields { + if let Fields::Named(fields) = &mut fields { + for field in &mut fields.named { + if matches!(field.vis, Visibility::Inherited) { + // Component implementations live in the parent module and need field access. + field.vis = syn::parse_quote!(pub(super)); + } + } + } + fields +} + +fn public_by_default(visibility: Visibility) -> Visibility { + if matches!(visibility, Visibility::Inherited) { + syn::parse_quote!(pub) + } else { + visibility + } +} + +pub fn expand(input: TokenStream) -> TokenStream { + let declaration = syn::parse_macro_input!(input as ComponentDeclaration); + let module_name = if let Some(module) = declaration.module.clone() { + module + } else { + let name = declaration.name.to_string().to_case(Case::Snake); + if let Err(error) = validate_generated_name(&name, declaration.name.span()) { + return error.into_compile_error().into(); + } + format_ident!("{name}") + }; + + if let Err(error) = validate(&declaration, &module_name) { + return error.into_compile_error().into(); + } + + let ComponentDeclaration { + visibility, + name, + generics, + state, + products, + .. + } = declaration; + let state = parent_visible_fields(state); + let visibility = public_by_default(visibility); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let products = products.into_iter().map(|product| { + let InstructionProduct { + attrs, + visibility: product_visibility, + name, + fields, + } = product; + let fields = public_fields(fields); + let product_visibility = public_by_default(product_visibility); + let product_generics = product_generics(&generics, &fields); + let (product_impl_generics, _, product_where_clause) = product_generics.split_for_impl(); + let declaration = match fields { + Fields::Unit => quote! { + #product_visibility struct #name #product_impl_generics #product_where_clause; + }, + Fields::Named(fields) => quote! { + #product_visibility struct #name #product_impl_generics #product_where_clause #fields + }, + Fields::Unnamed(fields) => quote! { + #product_visibility struct #name #product_impl_generics #fields #product_where_clause; + }, + }; + quote! { + #(#attrs)* + #declaration + } + }); + + quote! { + #visibility mod #module_name { + use super::*; + + #visibility struct #name #impl_generics #where_clause #state + + #visibility mod instruction { + use super::*; + + #( #products )* + } + } + } + .into() +} diff --git a/crates/vihaco-runtime-derive/src/composite.rs b/crates/vihaco-runtime-derive/src/composite.rs new file mode 100644 index 00000000..89aeae08 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite.rs @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +mod codegen; +mod loadable; +mod metadata; +mod syntax; +mod validate; + +use proc_macro::TokenStream; +use syntax::CompositeDeclaration; + +pub fn expand(input: TokenStream) -> TokenStream { + let declaration = syn::parse_macro_input!(input as CompositeDeclaration); + match codegen::try_expand(declaration) { + Ok(tokens) => tokens.into(), + Err(error) => error.into_compile_error().into(), + } +} + +#[cfg(test)] +mod tests { + use super::CompositeDeclaration; + use syn::parse_str; + + #[test] + fn parses_runtime_routes_and_handlers() { + let declaration: CompositeDeclaration = parse_str( + r#" + composite Cpu { + error = CpuFault; + stack: Stack, + alu: Alu, + debug: Debug, + } + runtime_instructions { + Add(AddInstruction) => alu { + message from stack; + effects { + observe debug; + absorb with stack; + } + } + Recv(RecvInstruction) => alu { + message with resolve_message; + effects { + handle with handle_receive; + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(declaration.routes.len(), 2); + assert!(matches!( + declaration.routes[0].message, + super::syntax::MessageSource::From(_) + )); + assert!(matches!( + declaration.routes[0].handler, + Some(super::syntax::Handler::Absorb(_)) + )); + assert!(matches!( + declaration.routes[1].handler, + Some(super::syntax::Handler::With(_)) + )); + } + + #[test] + fn parses_structural_composites_without_an_error_or_routes() { + let declaration: CompositeDeclaration = + parse_str(r#"composite Machine { clock: Clock, }"#).unwrap(); + assert!(declaration.error.is_none()); + assert!(declaration.routes.is_empty()); + } +} diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs new file mode 100644 index 00000000..ddf65165 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{Field, Generics, Ident, Result, Type}; + +use super::syntax::{CompositeDeclaration, Handler, MessageSource, RouteDeclaration}; +use crate::common::{resolve_root, retain_generics}; + +pub(super) fn retained_enum_generics(generics: &Generics, routes: &[RouteDeclaration]) -> Generics { + let payloads: Vec = routes + .iter() + .map(|route| { + let payload = &route.payload; + quote!(#payload) + }) + .collect(); + retain_generics(generics, &payloads) +} + +fn marker_ident(variant: &Ident) -> Ident { + let name = variant.to_string(); + let name = name.strip_prefix("r#").unwrap_or(&name); + format_ident!("__VihacoRoute_{name}") +} + +fn strip_consumed_field_attrs(mut field: Field) -> Field { + field.attrs.retain(|attr| { + !attr.path().is_ident("device") + && !attr.path().is_ident("loadable") + && !attr.path().is_ident("program") + }); + field +} + +pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result { + let root = resolve_root(&declaration.attrs)?; + let fields_metadata = super::validate::metadata_fields(&declaration.fields)?; + super::validate::validate_routes(&declaration.routes, &fields_metadata)?; + + let CompositeDeclaration { + mut attrs, + visibility, + name, + generics, + error, + fields, + routes, + } = declaration; + crate::common::strip_vihaco_attrs(&mut attrs); + let fields = fields.into_iter().map(strip_consumed_field_attrs); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let instruction_ident = format_ident!("{name}Instruction"); + let route_module = format_ident!("__Vihaco{name}Routes"); + + let instruction_declaration = if routes.is_empty() { + quote! {} + } else { + let enum_generics = retained_enum_generics(&generics, &routes); + let variants = routes.iter().map(|route| { + let variant = &route.variant; + let payload = &route.payload; + quote!(#variant(#payload)) + }); + quote! { + #[derive(Clone)] + #[allow(non_camel_case_types)] + pub enum #instruction_ident #enum_generics { + #( #variants ),* + } + } + }; + + let route_markers = routes.iter().map(|route| { + let marker = marker_ident(&route.variant); + quote! { + #[allow(non_camel_case_types)] + pub struct #marker; + } + }); + + let field_ty = |field: &Ident| -> &Type { + &fields_metadata + .iter() + .find(|candidate| candidate.ident == *field) + .expect("validated composite field") + .ty + }; + + let handle_impls = routes.iter().map(|route| { + let marker = marker_ident(&route.variant); + let target_ty = field_ty(&route.target); + let payload = &route.payload; + let effect = quote!(<#target_ty as #root::Execute<#payload>>::Effect); + let error_type = error.as_ref().expect("validated executable composite"); + let body = match route.handler.as_ref().expect("validated handler") { + Handler::Absorb(field) => { + let absorb_ty = field_ty(field); + quote! { + <#absorb_ty as #root::Absorb<#effect>>::absorb(&mut self.#field, effect) + .map_err(::std::convert::Into::<#error_type>::into) + } + } + Handler::With(method) => quote! { + self.#method(effect).map_err(::std::convert::Into::<#error_type>::into) + }, + }; + quote! { + impl #impl_generics #root::Handle<#effect, #route_module::#marker> + for #name #ty_generics #where_clause + { + type Error = #error_type; + + fn handle(&mut self, effect: #effect) -> ::std::result::Result<(), Self::Error> { + #body + } + } + } + }); + + let dispatch_arms = routes.iter().map(|route| { + let variant = &route.variant; + let target = &route.target; + let target_ty = field_ty(target); + let payload = &route.payload; + let marker = marker_ident(variant); + let error_type = error.as_ref().expect("validated executable composite"); + let message = match &route.message { + MessageSource::None => quote!(#root::NoMessage), + MessageSource::From(field) => { + let source_ty = field_ty(field); + quote! { + <#source_ty as #root::Supply< + <#target_ty as #root::Execute<#payload>>::Message + >>::supply(&mut self.#field) + .map_err(::std::convert::Into::<#error_type>::into)? + } + } + MessageSource::With(method) => quote! { + self.#method(instruction) + .map_err(::std::convert::Into::<#error_type>::into)? + }, + }; + let observers = route.observers.iter().map(|observer| { + let observer_ty = field_ty(observer); + quote! { + <#observer_ty as #root::Observe< + <#target_ty as #root::Execute<#payload>>::Effect, + #route_module::#marker + >>::observe(&mut self.#observer, &effect) + .map_err(::std::convert::Into::<#error_type>::into)?; + } + }); + quote! { + #instruction_ident::#variant(instruction) => { + let message = #message; + let result = <#target_ty as #root::Execute<#payload>>::execute( + &mut self.#target, + instruction, + message, + ) + .map_err(::std::convert::Into::<#error_type>::into)?; + for effect in result.effects { + #( #observers )* + >::Effect, + #route_module::#marker + >>::handle(self, effect) + .map_err(::std::convert::Into::<#error_type>::into)?; + } + Ok(result.execution) + } + } + }); + + let dispatch = if routes.is_empty() { + quote! {} + } else { + let error_type = error.as_ref().expect("validated executable composite"); + let enum_generics = retained_enum_generics(&generics, &routes); + let (_, enum_ty_generics, _) = enum_generics.split_for_impl(); + quote! { + #[allow(clippy::useless_conversion)] + fn execute_generated( + &mut self, + instruction: &#instruction_ident #enum_ty_generics, + ) -> ::std::result::Result<#root::Execution, #error_type> { + match instruction { + #( #dispatch_arms ),* + } + } + } + }; + + let metadata_impl = super::metadata::generate_metadata( + &root, + &name, + &generics, + &instruction_ident, + &routes, + &fields_metadata, + ); + let loadable_impl = + super::loadable::generate_loadable_impls(&root, &name, &generics, &fields_metadata); + + Ok(quote! { + #( #attrs )* + #visibility struct #name #impl_generics #where_clause { + #( #fields ),* + } + + #instruction_declaration + + #[doc(hidden)] + mod #route_module { + #( #route_markers )* + } + + impl #impl_generics #name #ty_generics #where_clause { + #dispatch + } + + #( #handle_impls )* + + #metadata_impl + #loadable_impl + }) +} diff --git a/crates/vihaco-runtime-derive/src/composite/loadable.rs b/crates/vihaco-runtime-derive/src/composite/loadable.rs new file mode 100644 index 00000000..f3625ae1 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite/loadable.rs @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{Generics, Ident}; + +use super::validate::FieldMetadata; + +pub(super) fn generate_loadable_impls( + root: &TokenStream2, + name: &Ident, + generics: &Generics, + fields: &[FieldMetadata], +) -> TokenStream2 { + let loadables: Vec<_> = fields + .iter() + .filter(|field| field.loadable.is_some()) + .collect(); + let context = format_ident!("__VihacoContext"); + + let (_, ty_generics, _) = generics.split_for_impl(); + let own_bytecode_predicate = quote! { + #name #ty_generics: #root::loader::LoadOwnBytecodeSection<#context> + }; + let own_sst_predicate = quote! { + #name #ty_generics: #root::loader::LoadOwnSstSection<#context> + }; + let bytecode_method_predicates: Vec<_> = loadables + .iter() + .map(|field| { + let field_ty = &field.ty; + quote! { #field_ty: #root::loader::LoadBytecodeSection<#context> } + }) + .collect(); + let sst_method_predicates: Vec<_> = loadables + .iter() + .map(|field| { + let field_ty = &field.ty; + quote! { #field_ty: #root::loader::LoadSstSection<#context> } + }) + .collect(); + let bytecode_children: Vec<_> = loadables + .iter() + .map(|field| { + let field_ident = &field.ident; + let field_ty = &field.ty; + let section_name = field.loadable.as_ref().expect("loadable field"); + quote! { + if let ::std::option::Option::Some(child) = section.child(#section_name) { + <#field_ty as #root::loader::LoadBytecodeSection<#context>> + ::load_bytecode_section(&mut self.#field_ident, child)?; + } + } + }) + .collect(); + let sst_children: Vec<_> = loadables + .iter() + .map(|field| { + let field_ident = &field.ident; + let field_ty = &field.ty; + let section_name = field.loadable.as_ref().expect("loadable field"); + quote! { + if let ::std::option::Option::Some(child) = section.child(#section_name) { + <#field_ty as #root::loader::LoadSstSection<#context>> + ::load_sst_section(&mut self.#field_ident, child)?; + } + } + }) + .collect(); + let loadable_names: Vec<_> = loadables + .iter() + .map(|field| field.loadable.as_ref().expect("loadable field").as_str()) + .collect(); + let expected_children = quote! { + let expected: &[&str] = &[#(#loadable_names),*]; + for child in section.children() { + let child_name = child.local_name().ok_or_else(|| { + ::eyre::eyre!( + "section `{}` yielded a root section as a child", + section.display_path(), + ) + })?; + if !expected.iter().any(|expected| *expected == child_name) { + return Err(::eyre::eyre!( + "section `{}` has unexpected child section `{}`", + section.display_path(), + child.display_path(), + )); + } + } + }; + + let mut bytecode_impl_generics = generics.clone(); + bytecode_impl_generics + .params + .push(syn::parse_quote!(#context)); + { + let where_clause = bytecode_impl_generics.make_where_clause(); + where_clause + .predicates + .push(syn::parse2(own_bytecode_predicate.clone()).expect("valid predicate")); + for field in &loadables { + let field_ty = &field.ty; + where_clause.predicates.push( + syn::parse2(quote! { + #field_ty: #root::loader::LoadBytecodeSection<#context> + }) + .expect("valid predicate"), + ); + } + } + let (bytecode_impl_generics, _, bytecode_where_clause) = + bytecode_impl_generics.split_for_impl(); + + let mut sst_impl_generics = generics.clone(); + sst_impl_generics.params.push(syn::parse_quote!(#context)); + { + let where_clause = sst_impl_generics.make_where_clause(); + where_clause + .predicates + .push(syn::parse2(own_sst_predicate.clone()).expect("valid predicate")); + for field in &loadables { + let field_ty = &field.ty; + where_clause.predicates.push( + syn::parse2(quote! { + #field_ty: #root::loader::LoadSstSection<#context> + }) + .expect("valid predicate"), + ); + } + } + let (sst_impl_generics, _, sst_where_clause) = sst_impl_generics.split_for_impl(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + quote! { + impl #impl_generics #name #ty_generics #where_clause { + pub fn load_generated_bytecode_sections<'__vihaco_bc, #context>( + &mut self, + section: #root::BytecodeSectionView<'__vihaco_bc, #context>, + ) -> ::eyre::Result<()> + where + #name #ty_generics: #root::loader::LoadOwnBytecodeSection<#context>, + #( #bytecode_method_predicates ),* + { + #root::loader::LoadOwnBytecodeSection::<#context>::load_own_bytecode_section( + self, + section.clone(), + )?; + #expected_children + #( #bytecode_children )* + Ok(()) + } + + pub fn load_generated_sst_sections<'__vihaco_sst, #context>( + &mut self, + section: #root::SstSectionView<'__vihaco_sst, #context>, + ) -> ::eyre::Result<()> + where + #name #ty_generics: #root::loader::LoadOwnSstSection<#context>, + #( #sst_method_predicates ),* + { + #root::loader::LoadOwnSstSection::<#context>::load_own_sst_section( + self, + section.clone(), + )?; + #expected_children + #( #sst_children )* + Ok(()) + } + } + + impl #bytecode_impl_generics #root::loader::LoadBytecodeSection<#context> + for #name #ty_generics + #bytecode_where_clause + { + fn load_bytecode_section<'__vihaco_bc>( + &mut self, + section: #root::BytecodeSectionView<'__vihaco_bc, #context>, + ) -> ::eyre::Result<()> { + self.load_generated_bytecode_sections(section) + } + } + + impl #sst_impl_generics #root::loader::LoadSstSection<#context> + for #name #ty_generics + #sst_where_clause + { + fn load_sst_section<'__vihaco_sst>( + &mut self, + section: #root::SstSectionView<'__vihaco_sst, #context>, + ) -> ::eyre::Result<()> { + self.load_generated_sst_sections(section) + } + } + } +} diff --git a/crates/vihaco-runtime-derive/src/composite/metadata.rs b/crates/vihaco-runtime-derive/src/composite/metadata.rs new file mode 100644 index 00000000..5fbcc1b0 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite/metadata.rs @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Generics, Ident, LitStr}; + +use super::syntax::RouteDeclaration; +use super::validate::FieldMetadata; + +pub(super) fn generate_metadata( + root: &TokenStream2, + name: &Ident, + generics: &Generics, + instruction_ident: &Ident, + routes: &[RouteDeclaration], + fields: &[FieldMetadata], +) -> TokenStream2 { + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let instruction_type = if routes.is_empty() { + quote! { () } + } else { + let enum_generics = super::codegen::retained_enum_generics(generics, routes); + let (_, enum_ty_generics, _) = enum_generics.split_for_impl(); + quote! { #instruction_ident #enum_ty_generics } + }; + let devices = fields.iter().filter_map(|field| { + field.device.as_ref().map(|device| { + let code = device.code; + let name = LitStr::new(&field.ident.to_string(), field.ident.span()); + quote! { + #root::metadata::DeviceMetadata { code: #code, name: #name } + } + }) + }); + let aliases = fields.iter().flat_map(|field| { + let code = field.device.as_ref().map(|device| device.code); + let field_aliases = field + .device + .as_ref() + .into_iter() + .flat_map(move |device| device.aliases.iter().map(move |alias| (code, alias))); + field_aliases.map(move |(code, alias)| { + let code = code.expect("device aliases have a device"); + quote! { + #root::metadata::SourceSymbolAliasMetadata { + name: #alias, + device_code: #code, + } + } + }) + }); + + quote! { + impl #impl_generics #root::__private::GeneratedMachine for #name #ty_generics #where_clause { + type Instruction = #instruction_type; + + fn metadata(&self) -> #root::CompositeMetadata { + static DEVICES: &[#root::metadata::DeviceMetadata] = &[ #( #devices ),* ]; + static SOURCE_SYMBOL_ALIASES: + &[#root::metadata::SourceSymbolAliasMetadata] = &[ #( #aliases ),* ]; + #root::CompositeMetadata { + devices: DEVICES, + source_symbol_aliases: SOURCE_SYMBOL_ALIASES, + } + } + } + } +} diff --git a/crates/vihaco-runtime-derive/src/composite/syntax.rs b/crates/vihaco-runtime-derive/src/composite/syntax.rs new file mode 100644 index 00000000..05e7a672 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite/syntax.rs @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use syn::parse::{Parse, ParseStream}; +use syn::{ + Attribute, Field, Generics, Ident, LitInt, LitStr, Result, Token, Type, Visibility, WhereClause, +}; + +use crate::common::parse_named_fields; + +syn::custom_keyword!(composite); +syn::custom_keyword!(error); +syn::custom_keyword!(runtime_instructions); +syn::custom_keyword!(message); +syn::custom_keyword!(effects); +syn::custom_keyword!(observe); +syn::custom_keyword!(absorb); +syn::custom_keyword!(handle); +syn::custom_keyword!(none); +syn::custom_keyword!(from); +syn::custom_keyword!(with); + +pub(super) struct CompositeDeclaration { + pub(super) attrs: Vec, + pub(super) visibility: Visibility, + pub(super) name: Ident, + pub(super) generics: Generics, + pub(super) error: Option, + pub(super) fields: Vec, + pub(super) routes: Vec, +} + +pub(super) struct RouteDeclaration { + pub(super) variant: Ident, + pub(super) payload: Type, + pub(super) target: Ident, + pub(super) message: MessageSource, + pub(super) observers: Vec, + pub(super) handler: Option, +} + +pub(super) enum MessageSource { + None, + From(Ident), + With(Ident), +} + +pub(super) enum Handler { + Absorb(Ident), + With(Ident), +} + +pub(super) struct DeviceArgs { + pub(super) code: u8, + pub(super) aliases: Vec, +} + +impl Parse for CompositeDeclaration { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = Attribute::parse_outer(input)?; + let visibility = input.parse()?; + input.parse::()?; + let name: Ident = input.parse()?; + + let mut generics: Generics = input.parse()?; + generics.where_clause = input.parse::>()?; + + let body; + syn::braced!(body in input); + let (error, fields) = parse_composite_body(&body)?; + + let routes = if input.peek(runtime_instructions) { + input.parse::()?; + let routes_body; + syn::braced!(routes_body in input); + parse_routes(&routes_body)? + } else { + Vec::new() + }; + + if !input.is_empty() { + return Err(input.error("unexpected tokens after composite declaration")); + } + + if !routes.is_empty() && error.is_none() { + return Err(syn::Error::new( + name.span(), + "executable composites require `error = ;`", + )); + } + + Ok(Self { + attrs, + visibility, + name, + generics, + error, + fields, + routes, + }) + } +} + +fn parse_composite_body(input: ParseStream<'_>) -> Result<(Option, Vec)> { + let mut error_type = None; + + if input.peek(error) { + input.parse::()?; + input.parse::()?; + error_type = Some(input.parse::()?); + input.parse::()?; + } + + let fields = parse_named_fields(input)?.into_iter().collect(); + + Ok((error_type, fields)) +} + +fn parse_routes(input: ParseStream<'_>) -> Result> { + let mut routes = Vec::new(); + while !input.is_empty() { + routes.push(input.parse::()?); + if input.peek(Token![,]) { + input.parse::()?; + } + } + Ok(routes) +} + +impl Parse for RouteDeclaration { + fn parse(input: ParseStream<'_>) -> Result { + let variant: Ident = input.parse()?; + let payload_content; + syn::parenthesized!(payload_content in input); + let payload = payload_content.parse()?; + input.parse::]>()?; + let target = input.parse()?; + + let body; + syn::braced!(body in input); + + let mut message_source = None; + let mut observers = Vec::new(); + let mut handler = None; + let mut saw_effects = false; + + while !body.is_empty() { + if body.peek(message) { + body.parse::()?; + if message_source.is_some() { + return Err(body.error("route has more than one message clause")); + } + let source = if body.peek(none) { + body.parse::()?; + MessageSource::None + } else if body.peek(from) { + body.parse::()?; + MessageSource::From(body.parse()?) + } else if body.peek(with) { + body.parse::()?; + MessageSource::With(body.parse()?) + } else { + return Err(body.error("expected `none`, `from `, or `with `")); + }; + body.parse::()?; + message_source = Some(source); + } else if body.peek(effects) { + body.parse::()?; + if saw_effects { + return Err(body.error("route has more than one effects block")); + } + saw_effects = true; + let effects_body; + syn::braced!(effects_body in body); + parse_effects(&effects_body, &mut observers, &mut handler)?; + } else { + return Err(body.error("expected a message clause or effects block")); + } + } + + let message = message_source + .ok_or_else(|| syn::Error::new(variant.span(), "route is missing a message clause"))?; + if !saw_effects { + return Err(syn::Error::new( + variant.span(), + "route is missing an effects block", + )); + } + + Ok(Self { + variant, + payload, + target, + message, + observers, + handler, + }) + } +} + +fn parse_effects( + input: ParseStream<'_>, + observers: &mut Vec, + handler: &mut Option, +) -> Result<()> { + while !input.is_empty() { + if input.peek(observe) { + input.parse::()?; + let mut names = Vec::new(); + loop { + names.push(input.parse::()?); + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + if names.is_empty() { + return Err(input.error("`observe` requires at least one field")); + } + observers.extend(names); + input.parse::()?; + } else if input.peek(absorb) || input.peek(handle) { + let is_absorb = input.peek(absorb); + if is_absorb { + input.parse::()?; + } else { + input.parse::()?; + } + input.parse::()?; + let method_or_field: Ident = input.parse()?; + if handler.is_some() { + return Err(syn::Error::new( + method_or_field.span(), + "route has more than one effect handler", + )); + } + *handler = Some(if is_absorb { + Handler::Absorb(method_or_field) + } else { + Handler::With(method_or_field) + }); + input.parse::()?; + } else { + return Err(input.error( + "expected `observe `, `absorb with `, or `handle with `", + )); + } + } + + if handler.is_none() { + return Err(input.error("effects block is missing an effect handler")); + } + Ok(()) +} + +impl Parse for DeviceArgs { + fn parse(input: ParseStream<'_>) -> Result { + let literal: LitInt = input.parse()?; + let code = literal.base10_parse::()?; + let mut aliases = Vec::new(); + while input.peek(Token![,]) { + input.parse::()?; + if input.is_empty() { + break; + } + let key: Ident = input.parse()?; + input.parse::()?; + if key != "alias" { + return Err(syn::Error::new(key.span(), "unsupported device argument")); + } + aliases.push(input.parse()?); + } + Ok(Self { code, aliases }) + } +} diff --git a/crates/vihaco-runtime-derive/src/composite/validate.rs b/crates/vihaco-runtime-derive/src/composite/validate.rs new file mode 100644 index 00000000..0b3ea731 --- /dev/null +++ b/crates/vihaco-runtime-derive/src/composite/validate.rs @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use proc_macro2::Span; +use std::collections::{BTreeMap, BTreeSet}; +use syn::spanned::Spanned; +use syn::{Field, Ident, LitStr, Result, Type}; + +use super::syntax::{DeviceArgs, Handler, MessageSource, RouteDeclaration}; + +pub(super) struct FieldMetadata { + pub(super) ident: Ident, + pub(super) ty: Type, + pub(super) device: Option, + pub(super) loadable: Option, +} + +fn validate_loadable_name(name: &str, span: Span) -> Result<()> { + if name.is_empty() { + return Err(syn::Error::new( + span, + "loadable section name cannot be empty", + )); + } + if name.contains('/') { + return Err(syn::Error::new( + span, + "loadable section name cannot contain `/`", + )); + } + Ok(()) +} + +pub(super) fn metadata_fields(fields: &[Field]) -> Result> { + let mut metadata = Vec::with_capacity(fields.len()); + for field in fields { + let ident = field + .ident + .clone() + .ok_or_else(|| syn::Error::new(field.span(), "composite fields must be named"))?; + let mut device = None; + let mut loadable = None; + for attr in &field.attrs { + if attr.path().is_ident("device") { + if device.is_some() { + return Err(syn::Error::new( + attr.span(), + format!("duplicate device attribute on field `{ident}`"), + )); + } + device = Some(attr.parse_args::()?); + } else if attr.path().is_ident("loadable") { + if loadable.is_some() { + return Err(syn::Error::new( + attr.span(), + format!("duplicate loadable attribute on field `{ident}`"), + )); + } + let name = if matches!(&attr.meta, syn::Meta::Path(_)) { + ident.to_string() + } else { + attr.parse_args::()?.value() + }; + validate_loadable_name(&name, attr.span())?; + loadable = Some(name); + } + } + if loadable.is_some() && device.is_none() { + return Err(syn::Error::new( + ident.span(), + format!("field `{ident}` marked #[loadable] must also be marked #[device(...)]"), + )); + } + metadata.push(FieldMetadata { + ident, + ty: field.ty.clone(), + device, + loadable, + }); + } + + let mut device_codes = BTreeMap::::new(); + let mut source_symbols = BTreeMap::::new(); + let mut loadable_names = BTreeMap::::new(); + for field in &metadata { + let Some(device) = &field.device else { + continue; + }; + if let Some(previous) = device_codes.insert(device.code, field.ident.clone()) { + return Err(syn::Error::new( + field.ident.span(), + format!( + "duplicate device code 0x{:02X} for fields `{previous}` and `{}`", + device.code, field.ident + ), + )); + } + insert_source_symbol(&mut source_symbols, field.ident.to_string(), &field.ident)?; + let mut aliases = BTreeSet::new(); + for alias in &device.aliases { + let name = alias.value(); + if !aliases.insert(name.clone()) { + return Err(syn::Error::new( + alias.span(), + format!("duplicate alias `{name}` on field `{}`", field.ident), + )); + } + insert_source_symbol(&mut source_symbols, name, &field.ident)?; + } + if let Some(name) = &field.loadable + && let Some(previous) = loadable_names.insert(name.clone(), field.ident.clone()) + { + return Err(syn::Error::new( + field.ident.span(), + format!( + "duplicate loadable section name `{name}` for fields `{previous}` and `{}`", + field.ident + ), + )); + } + } + Ok(metadata) +} + +fn insert_source_symbol( + symbols: &mut BTreeMap, + name: String, + field: &Ident, +) -> Result<()> { + if let Some(previous) = symbols.insert(name.clone(), field.clone()) { + return Err(syn::Error::new( + field.span(), + format!("duplicate source symbol `{name}` for `{previous}` and `{field}`"), + )); + } + Ok(()) +} + +pub(super) fn validate_routes(routes: &[RouteDeclaration], fields: &[FieldMetadata]) -> Result<()> { + let field_names: BTreeSet<_> = fields.iter().map(|field| field.ident.to_string()).collect(); + let mut variants = BTreeSet::new(); + for route in routes { + if !variants.insert(route.variant.to_string()) { + return Err(syn::Error::new( + route.variant.span(), + format!("duplicate runtime instruction variant `{}`", route.variant), + )); + } + if !field_names.contains(&route.target.to_string()) { + return Err(syn::Error::new( + route.target.span(), + format!("unknown composite field `{}`", route.target), + )); + } + match &route.message { + MessageSource::From(field) if !field_names.contains(&field.to_string()) => { + return Err(syn::Error::new( + field.span(), + format!("unknown composite field `{field}`"), + )); + } + _ => {} + } + let mut observer_names = BTreeSet::new(); + for observer in &route.observers { + if !field_names.contains(&observer.to_string()) { + return Err(syn::Error::new( + observer.span(), + format!("unknown observer field `{observer}`"), + )); + } + if !observer_names.insert(observer.to_string()) { + return Err(syn::Error::new( + observer.span(), + format!("duplicate observer field `{observer}`"), + )); + } + } + if let Some(Handler::Absorb(field)) = &route.handler + && !field_names.contains(&field.to_string()) + { + return Err(syn::Error::new( + field.span(), + format!("unknown effect destination field `{field}`"), + )); + } + } + Ok(()) +} diff --git a/crates/vihaco-runtime-derive/src/derive_message.rs b/crates/vihaco-runtime-derive/src/derive_message.rs deleted file mode 100644 index 9a11ddae..00000000 --- a/crates/vihaco-runtime-derive/src/derive_message.rs +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use proc_macro::TokenStream; -use quote::quote; -use syn::DeriveInput; - -use crate::common::resolve_root; - -pub fn expand(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - let root = match resolve_root(&input.attrs) { - Ok(root) => root, - Err(err) => return err.into_compile_error().into(), - }; - let ident = input.ident; - quote! { - impl #root::runtime::Message for #ident {} - } - .into() -} diff --git a/crates/vihaco-runtime-derive/src/lib.rs b/crates/vihaco-runtime-derive/src/lib.rs index bc70c28c..64b8b47b 100644 --- a/crates/vihaco-runtime-derive/src/lib.rs +++ b/crates/vihaco-runtime-derive/src/lib.rs @@ -1,62 +1,56 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -mod attr_component; -mod attr_composite; -mod attr_observe; mod common; -mod derive_message; +mod component; +mod composite; -use crate::common::strip_vihaco_attrs; use proc_macro::TokenStream; -use quote::quote; -use syn::{Data, DeriveInput, Fields}; -#[proc_macro_derive(Message, attributes(vihaco))] -pub fn derive_message(input: TokenStream) -> TokenStream { - derive_message::expand(input) +#[proc_macro] +pub fn composite(input: TokenStream) -> TokenStream { + composite::expand(input) } -#[proc_macro_attribute] -pub fn composite(_attr: TokenStream, item: TokenStream) -> TokenStream { - let original = proc_macro2::TokenStream::from(item.clone()); - let generated = proc_macro2::TokenStream::from(attr_composite::expand(item)); - let mut sanitized: DeriveInput = match syn::parse2(original) { - Ok(input) => input, - Err(err) => return err.into_compile_error().into(), - }; - - strip_vihaco_attrs(&mut sanitized.attrs); - - if let Data::Struct(data) = &mut sanitized.data - && let Fields::Named(fields) = &mut data.fields - { - for field in &mut fields.named { - field.attrs.retain(|attr| { - let path = attr.path(); - !(path.is_ident("device") || path.is_ident("loadable")) - }); - } - } - - quote! { - #sanitized - #generated - } - .into() -} - -#[proc_macro_attribute] -pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream { - attr_component::expand(attr, item) -} - -#[proc_macro_attribute] -pub fn machine(_attr: TokenStream, item: TokenStream) -> TokenStream { - item -} - -#[proc_macro_attribute] -pub fn observe(attr: TokenStream, item: TokenStream) -> TokenStream { - attr_observe::expand(attr, item) +#[proc_macro] +/// Declares a reusable runtime component and its instruction types. +/// +/// The component state is declared in the first block. An optional `instruction` block declares +/// the owned runtime instruction types that can be executed by the component. The macro generates +/// a public module whose name is the snake-case form of the component name; instruction types are +/// nested in that module's `instruction` namespace. +/// +/// State fields without an explicit visibility are available to component implementations in +/// the surrounding module through `pub(super)`. Instruction fields without an explicit visibility +/// are public so composite-generated code can construct them. Names used in state and instruction +/// fields are resolved from the module containing the macro invocation. +/// +/// `component!` does not define source syntax, select a machine's instruction set, generate a +/// dispatch implementation, or implement `Execute`. Those responsibilities belong to the +/// composite and component implementation. +/// +/// # Example +/// +/// ``` +/// use vihaco_runtime_derive::component; +/// +/// component! { +/// component Counter { +/// value: u64, +/// } +/// +/// instruction { +/// Add(u64), +/// Reset, +/// } +/// } +/// +/// let _: counter::instruction::Add = counter::instruction::Add(1); +/// let _: counter::instruction::Reset = counter::instruction::Reset; +/// let _: counter::Counter = counter::Counter { value: 0 }; +/// ``` +/// +/// A component with no runtime instruction types may omit the `instruction` block entirely. +pub fn component(input: TokenStream) -> TokenStream { + component::expand(input) } diff --git a/crates/vihaco-runtime/src/execute.rs b/crates/vihaco-runtime/src/execute.rs new file mode 100644 index 00000000..82d499fa --- /dev/null +++ b/crates/vihaco-runtime/src/execute.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use crate::Effects; + +/// Marker message for instructions whose execution does not require a +/// runtime-supplied message. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct NoMessage; + +/// Outcome of one instruction step. +/// +/// This is independent of any timing model. It answers whether the parent +/// may advance the program counter or must keep the composite parked. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Execution { + /// The step resolved; the parent may advance to the next instruction. + Complete, + /// The step is unresolved; the parent must wait for a completion. + Parked, +} + +/// The standardized result of starting or resuming one instruction route. +/// Effects are handled independently from the route's completion state. +pub struct StepResult { + pub effects: Effects, + pub execution: Execution, +} + +/// A component executes one fully-resolved runtime instruction against its +/// own state. +pub trait Execute { + type Message; + type Effect; + type Fault; + + fn execute( + &mut self, + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} diff --git a/crates/vihaco-runtime/src/generated.rs b/crates/vihaco-runtime/src/generated.rs index 73677a54..adc9b60c 100644 --- a/crates/vihaco-runtime/src/generated.rs +++ b/crates/vihaco-runtime/src/generated.rs @@ -5,18 +5,6 @@ use eyre::Result; use crate::Effects; -pub trait GeneratedComponent { - type Instruction; - type Message; - type Effect; - - fn execute_generated( - &mut self, - inst: Self::Instruction, - msg: Self::Message, - ) -> Result>; -} - pub fn expect_exactly_one_effect(effects: Effects) -> Result { let mut iter = effects.into_iter(); let first = iter.next(); diff --git a/crates/vihaco-runtime/src/handle.rs b/crates/vihaco-runtime/src/handle.rs new file mode 100644 index 00000000..38a581b4 --- /dev/null +++ b/crates/vihaco-runtime/src/handle.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// A reusable, machine-agnostic capability: this component knows how to +/// consume an effect of the specified type. +pub trait Absorb { + type Fault; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; +} + +/// Effect handling selected by a composite-specific route marker. +pub trait Handle { + type Error; + + fn handle(&mut self, effect: E) -> Result<(), Self::Error>; +} diff --git a/crates/vihaco-runtime/src/lib.rs b/crates/vihaco-runtime/src/lib.rs index 068eb253..6b488d8d 100644 --- a/crates/vihaco-runtime/src/lib.rs +++ b/crates/vihaco-runtime/src/lib.rs @@ -3,9 +3,12 @@ extern crate self as vihaco_runtime; +mod execute; mod generated; +mod handle; mod marker; mod observe; +mod supply; #[doc(hidden)] pub mod __private; @@ -18,21 +21,23 @@ pub use vihaco_abi::{Effects, metadata}; pub use vihaco_bytecode::{BytecodeSectionView, SstSectionView}; pub use vihaco_module::loader; -pub use generated::{CompositeMetadata, GeneratedComponent, expect_exactly_one_effect}; +pub use execute::{Execute, Execution, NoMessage, StepResult}; +pub use generated::{CompositeMetadata, expect_exactly_one_effect}; +pub use handle::{Absorb, Handle}; pub use marker::Message; pub use observe::Observe; +pub use supply::Supply; -// `#[derive(Message)]` emits `#root::runtime::Message`; keep a `runtime` segment -// available on this crate too (the facade exposes it via `pub use vihaco_runtime -// as runtime;`) so direct dependents resolve the marker identically. +// Keep a `runtime` segment available on this crate too (the facade exposes it +// via `pub use vihaco_runtime as runtime;`) for generated code. pub use crate as runtime; // The `Instruction` derive lives in `vihaco-abi(-derive)`; re-export it here so -// `#[composite]`'s generated `#root::Instruction` derive resolves through the +// `composite!` generated instruction declarations resolve through the // runtime root as well. #[cfg(feature = "derive")] pub use vihaco_abi::Instruction; // Re-export the runtime derives behind the `derive` feature (serde convention). #[cfg(feature = "derive")] -pub use vihaco_runtime_derive::{Message, component, composite, machine, observe}; +pub use vihaco_runtime_derive::{component, composite}; diff --git a/crates/vihaco-runtime/src/observe.rs b/crates/vihaco-runtime/src/observe.rs index 2add65f6..178170fd 100644 --- a/crates/vihaco-runtime/src/observe.rs +++ b/crates/vihaco-runtime/src/observe.rs @@ -3,8 +3,11 @@ use crate::Effects; -pub trait Observe { - type Effect: 'static; +/// A non-consuming effect observer selected by a composite-specific route marker. +/// +/// Composite route generation currently discards follow-up effects. +pub trait Observe { + type Effect; type Error; fn observe(&mut self, effect: &E) -> Result, Self::Error>; diff --git a/crates/vihaco-runtime/src/supply.rs b/crates/vihaco-runtime/src/supply.rs new file mode 100644 index 00000000..c4bb9fbd --- /dev/null +++ b/crates/vihaco-runtime/src/supply.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +/// The dual of [`Absorb`](crate::Absorb): this component knows how to hand +/// out a message of the specified type. +pub trait Supply { + type Fault; + + fn supply(&mut self) -> Result; +} diff --git a/crates/vihaco-runtime/tests/runtime_contract.rs b/crates/vihaco-runtime/tests/runtime_contract.rs new file mode 100644 index 00000000..1bfa8295 --- /dev/null +++ b/crates/vihaco-runtime/tests/runtime_contract.rs @@ -0,0 +1,112 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use vihaco_runtime::{ + Absorb, Effects, Execute, Execution, Handle, NoMessage, Observe, StepResult, Supply, +}; + +#[derive(Debug, PartialEq, Eq)] +struct Message(u8); + +#[derive(Debug, PartialEq, Eq)] +struct Effect(u8); + +#[derive(Debug, PartialEq, Eq)] +struct Fault; + +#[derive(Debug, PartialEq, Eq)] +struct Route; + +#[derive(Default)] +struct Component { + supplied: u8, + absorbed: Vec, + observed: Vec, + handled: Vec, +} + +impl Supply for Component { + type Fault = Fault; + + fn supply(&mut self) -> Result { + Ok(Message(self.supplied)) + } +} + +impl Absorb for Component { + type Fault = Fault; + + fn absorb(&mut self, effect: Effect) -> Result<(), Self::Fault> { + self.absorbed.push(effect.0); + Ok(()) + } +} + +impl Observe for Component { + type Effect = (); + type Error = Fault; + + fn observe(&mut self, effect: &Effect) -> Result, Self::Error> { + self.observed.push(effect.0); + Ok(Effects::none()) + } +} + +impl Handle for Component { + type Error = Fault; + + fn handle(&mut self, effect: Effect) -> Result<(), Self::Error> { + self.handled.push(effect.0); + Ok(()) + } +} + +impl Execute for Component { + type Message = NoMessage; + type Effect = Effect; + type Fault = Fault; + + fn execute( + &mut self, + instruction: &u8, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(Effect(*instruction)), + execution: Execution::Complete, + }) + } +} + +#[test] +fn execute_returns_effects_and_execution_state() { + let mut component = Component::default(); + let result = component.execute(&7, NoMessage).unwrap(); + + assert_eq!(result.effects, Effects::one(Effect(7))); + assert_eq!(result.execution, Execution::Complete); +} + +#[test] +fn supply_absorb_observe_and_handle_are_route_capabilities() { + let mut component = Component { + supplied: 3, + ..Component::default() + }; + + assert_eq!(component.supply().unwrap(), Message(3)); + + let effect = Effect(9); + component.observe(&effect).unwrap(); + component.absorb(effect).unwrap(); + component.handle(Effect(11)).unwrap(); + + assert_eq!(component.observed, vec![9]); + assert_eq!(component.absorbed, vec![9]); + assert_eq!(component.handled, vec![11]); +} + +#[test] +fn execution_has_complete_and_parked_states() { + assert_ne!(Execution::Complete, Execution::Parked); +} diff --git a/crates/vihaco-stdlib/src/observer/stdio.rs b/crates/vihaco-stdlib/src/observer/stdio.rs index 7fc89586..7c084082 100644 --- a/crates/vihaco-stdlib/src/observer/stdio.rs +++ b/crates/vihaco-stdlib/src/observer/stdio.rs @@ -4,7 +4,7 @@ use std::io::Write; use eyre::Result; -use vihaco_runtime::{Effects, observe}; +use vihaco_runtime::{Effects, Observe}; #[derive(Debug, Clone)] pub struct StdoutEffect(pub String); @@ -25,9 +25,11 @@ impl StdoutObserver { } } -#[observe(StdoutEffect)] -impl StdoutObserver { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +impl Observe for StdoutObserver { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.write_stdout(&effect.0)?; Ok(Effects::none()) } diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index 6ea75aa3..d4cf066c 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -38,11 +38,11 @@ pub use instruction_syntax::{ pub use loader::{ LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, LoadSstSection, ProgramImage, }; -pub use macros::{Instruction, Message, component, composite, observe}; +pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; pub use runtime::{ - CompositeMetadata, EffectSink, GeneratedComponent, Message as MessageMarker, Observe, - expect_exactly_one_effect, + Absorb, CompositeMetadata, EffectSink, Execute, Execution, Handle, Message, + Message as MessageMarker, NoMessage, Observe, StepResult, Supply, expect_exactly_one_effect, }; pub use traits::{FromBytes, FromText, GetProgramInfo, Reset}; pub use vihaco_parser::SurfaceInstruction; @@ -50,9 +50,9 @@ pub use vihaco_parser::SurfaceInstruction; #[cfg(test)] mod public_api_tests { use crate::{ - BytecodeGlobalContext, BytecodeHeader, ConstantId, EffectSink, Effects, GeneratedComponent, + BytecodeGlobalContext, BytecodeHeader, ConstantId, EffectSink, Effects, Execute, Execution, GlobalContext, LoadBytecodeSection, LoadOwnBytecodeSection, Reset, SectionNameResolver, - SstGlobalContext, SstHeader, WriteBytecodeHeader, + SstGlobalContext, SstHeader, StepResult, WriteBytecodeHeader, instruction::{FromBytes, OpCode, WriteBytes}, module::FunctionInfo, observer::stdio::StdoutEffect, @@ -154,24 +154,27 @@ mod public_api_tests { #[derive(Clone, Copy)] struct DemoComponent; - impl GeneratedComponent for DemoComponent { - type Instruction = (); + impl Execute<()> for DemoComponent { type Message = (); type Effect = u8; + type Fault = eyre::Report; - fn execute_generated( + fn execute( &mut self, - _inst: Self::Instruction, + _inst: &(), _msg: Self::Message, - ) -> eyre::Result> { - Ok(Effects::one(7)) + ) -> eyre::Result> { + Ok(StepResult { + effects: Effects::one(7), + execution: Execution::Complete, + }) } } #[test] - fn generated_component_executes_without_exec_context() { + fn execute_component_without_exec_context() { let mut component = DemoComponent; - let effects = GeneratedComponent::execute_generated(&mut component, (), ()).unwrap(); + let effects = Execute::execute(&mut component, &(), ()).unwrap().effects; assert_eq!(effects, Effects::one(7)); assert_eq!(crate::expect_exactly_one_effect(effects).unwrap(), 7); diff --git a/crates/vihaco/src/macros/mod.rs b/crates/vihaco/src/macros/mod.rs index c5c4043a..972a2e81 100644 --- a/crates/vihaco/src/macros/mod.rs +++ b/crates/vihaco/src/macros/mod.rs @@ -2,4 +2,4 @@ // SPDX-License-Identifier: MIT pub use vihaco_abi::Instruction; -pub use vihaco_runtime::{Message, component, composite, observe}; +pub use vihaco_runtime::{component, composite}; diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs index 5392aa96..eac7018b 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs @@ -1,29 +1,12 @@ -use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; - -#[derive(Instruction)] -enum DemoInst { - Run, -} - -#[derive(Message)] -struct DemoMsg; - struct DemoDevice; -#[component(instruction = DemoInst, message = DemoMsg)] -impl DemoDevice { - fn execute(&mut self, _inst: DemoInst, _msg: DemoMsg) -> Result> { - Ok(Effects::none()) - } -} - -#[vihaco::composite] -struct BadMachine { +vihaco::composite! { +composite BadMachine { #[device(0x01)] a: DemoDevice, #[device(0x01)] b: DemoDevice, } +} fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr index f7c47c55..eef4dcdf 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr @@ -1,5 +1,5 @@ error: duplicate device code 0x01 for fields `a` and `b` - --> tests/compile_fail/composite_machine/duplicate-device-code.rs:26:5 - | -26 | b: DemoDevice, - | ^ + --> tests/compile_fail/composite_machine/duplicate-device-code.rs:8:5 + | +8 | b: DemoDevice, + | ^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs index 636daea0..0465e859 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs @@ -1,7 +1,7 @@ struct Child; -#[vihaco::composite] -struct BadMachine { +vihaco::composite! { +composite BadMachine { #[device(0x01)] #[loadable("child")] a: Child, @@ -9,5 +9,6 @@ struct BadMachine { #[loadable("child")] b: Child, } +} fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs index 997dbdfc..98add060 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs +++ b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs @@ -1,10 +1,11 @@ struct Child; -#[vihaco::composite] -struct BadMachine { +vihaco::composite! { +composite BadMachine { #[device(0x01)] #[loadable("child/nested")] child: Child, } +} fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr index 4b6a8651..70f1950f 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr +++ b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr @@ -1,5 +1,5 @@ error: loadable section name cannot contain `/` - --> tests/compile_fail/composite_machine/invalid-loadable-name.rs:6:16 + --> tests/compile_fail/composite_machine/invalid-loadable-name.rs:6:5 | 6 | #[loadable("child/nested")] - | ^^^^^^^^^^^^^^ + | ^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs index 50e4a13d..665fe841 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs +++ b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs @@ -1,9 +1,10 @@ struct Child; -#[vihaco::composite] -struct BadMachine { +vihaco::composite! { +composite BadMachine { #[loadable("child")] child: Child, } +} fn main() {} diff --git a/crates/vihaco/tests/component_macro.rs b/crates/vihaco/tests/component_macro.rs new file mode 100644 index 00000000..791ad11b --- /dev/null +++ b/crates/vihaco/tests/component_macro.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use vihaco::component; + +pub struct ParentContext; + +component! { + component UsesParentContext { + context: ParentContext, + } + + instruction { + ParentProduct { context: ParentContext }, + } +} + +component! { + component ComponentWithoutInstructions {} +} + +component! { + component GenericComponent + where + T: Clone, + { + _value: T, + } + + instruction { + Unit, + Tuple(T), + Named { value: T }, + Array([T; N]), + } +} + +#[test] +fn components_without_instructions_still_generate_the_component_module() { + let _: component_without_instructions::ComponentWithoutInstructions = + component_without_instructions::ComponentWithoutInstructions {}; +} + +#[test] +fn generated_modules_can_use_names_from_the_parent_module() { + let _: uses_parent_context::UsesParentContext = uses_parent_context::UsesParentContext { + context: ParentContext, + }; + let _: uses_parent_context::instruction::ParentProduct = + uses_parent_context::instruction::ParentProduct { + context: ParentContext, + }; +} + +#[test] +fn generated_products_support_all_field_forms() { + let _: generic_component::instruction::Unit = generic_component::instruction::Unit; + let _: generic_component::instruction::Tuple = generic_component::instruction::Tuple(1); + let _: generic_component::instruction::Named = + generic_component::instruction::Named { value: 1 }; + let _: generic_component::instruction::Array = + generic_component::instruction::Array([1, 2]); + let _: core::marker::PhantomData> = + core::marker::PhantomData; +} diff --git a/crates/vihaco/tests/multisection_bytecode.rs b/crates/vihaco/tests/multisection_bytecode.rs index d48847d8..fb1f8702 100644 --- a/crates/vihaco/tests/multisection_bytecode.rs +++ b/crates/vihaco/tests/multisection_bytecode.rs @@ -5,10 +5,10 @@ use std::{io::Read, str::FromStr}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use vihaco::{ - BytecodeFile, BytecodeGlobalContext, BytecodeSectionView, ConstantId, Effects, FLAGS, - GeneratedComponent, GetProgramInfo, Instruction, LoadBytecodeSection, LoadOwnBytecodeSection, - LoadOwnSstSection, LoadSstSection, MAGIC, ProgramImage, SectionNameResolver, SstFile, - SstGlobalContext, SstHeader, SstSectionView, Type, VERSION, Value, + BytecodeFile, BytecodeGlobalContext, BytecodeSectionView, ConstantId, FLAGS, GetProgramInfo, + Instruction, LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, LoadSstSection, + MAGIC, ProgramImage, SectionNameResolver, SstFile, SstGlobalContext, SstHeader, SstSectionView, + Type, VERSION, Value, module::LocalModule, syntax::{ParsedModule, Resolve}, traits::{FromBytes, FromText, WriteBytes}, @@ -184,34 +184,6 @@ struct TextLoadedDevice { program: TextProgram, } -impl GeneratedComponent for LoadedDevice { - type Instruction = TestInst; - type Message = (); - type Effect = (); - - fn execute_generated( - &mut self, - _inst: Self::Instruction, - _msg: Self::Message, - ) -> eyre::Result> { - Ok(Effects::none()) - } -} - -impl GeneratedComponent for TextLoadedDevice { - type Instruction = TextInst; - type Message = (); - type Effect = (); - - fn execute_generated( - &mut self, - _inst: Self::Instruction, - _msg: Self::Message, - ) -> eyre::Result> { - Ok(Effects::none()) - } -} - impl LoadBytecodeSection for LoadedDevice { fn load_bytecode_section<'bc>( &mut self, @@ -232,10 +204,10 @@ impl LoadSstSection for TextLoadedDevice { } } -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct Machine { +composite Machine { program: BytecodeProgram, #[device(0x01)] @@ -246,47 +218,36 @@ struct Machine { #[loadable] default_child: LoadedDevice, } +} -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct NestedMachine { +composite NestedMachine { program: BytecodeProgram, #[device(0x01)] #[loadable("leaf")] leaf: LoadedDevice, } - -impl GeneratedComponent for NestedMachine { - type Instruction = NestedMachineInstruction; - type Message = (); - type Effect = (); - - fn execute_generated( - &mut self, - _inst: Self::Instruction, - _msg: Self::Message, - ) -> eyre::Result> { - Ok(Effects::none()) - } } -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct HostMachine { +composite HostMachine { program: BytecodeProgram, #[device(0x01)] #[loadable("middle")] middle: NestedMachine, } +} -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct HeaderMachine { +composite HeaderMachine { info: TestHeader, program: BytecodeProgram, @@ -294,11 +255,12 @@ struct HeaderMachine { #[device(0x01)] device: LoadedDevice, } +} -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct TextMachine { +composite TextMachine { program: TextProgram, #[device(0x01)] @@ -309,47 +271,36 @@ struct TextMachine { #[loadable] default_child: TextLoadedDevice, } +} -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct TextNestedMachine { +composite TextNestedMachine { program: TextProgram, #[device(0x01)] #[loadable("leaf")] leaf: TextLoadedDevice, } - -impl GeneratedComponent for TextNestedMachine { - type Instruction = TextNestedMachineInstruction; - type Message = (); - type Effect = (); - - fn execute_generated( - &mut self, - _inst: Self::Instruction, - _msg: Self::Message, - ) -> eyre::Result> { - Ok(Effects::none()) - } } -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct TextHostMachine { +composite TextHostMachine { program: TextProgram, #[device(0x01)] #[loadable("middle")] middle: TextNestedMachine, } +} -#[vihaco::composite] +vihaco::composite! { #[derive(Debug, Default)] #[allow(dead_code)] -struct TextHeaderMachine { +composite TextHeaderMachine { info: TestHeader, program: TextProgram, @@ -357,6 +308,7 @@ struct TextHeaderMachine { #[device(0x01)] device: TextLoadedDevice, } +} impl LoadOwnBytecodeSection for Machine { fn load_own_bytecode_section<'bc>( diff --git a/crates/vihaco/tests/rfc0008_observe_macro.rs b/crates/vihaco/tests/rfc0008_observe_macro.rs deleted file mode 100644 index 8fcad35d..00000000 --- a/crates/vihaco/tests/rfc0008_observe_macro.rs +++ /dev/null @@ -1,329 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::{SystemTime, UNIX_EPOCH}; - -use vihaco::{Effects, Observe, observe}; - -#[derive(Debug, Clone)] -struct TestEffect(pub i32); - -#[derive(Default)] -struct TestObserver { - received: Vec, -} - -#[observe(TestEffect, effect = ())] -impl TestObserver { - fn observe_test_effect(&mut self, effect: &TestEffect) -> eyre::Result> { - self.received.push(effect.0); - Ok(Effects::none()) - } -} - -#[test] -fn observe_macro_generates_trait_impl() { - let mut obs = TestObserver::default(); - let follow_ups = Observe::::observe(&mut obs, &TestEffect(42)).unwrap(); - assert_eq!(obs.received, vec![42]); - assert!(follow_ups.into_iter().next().is_none()); -} - -#[derive(Debug, Clone)] -struct AnotherEffect(pub String); - -#[derive(Default)] -struct MultiObserver { - ints: Vec, - strings: Vec, -} - -#[observe(TestEffect, AnotherEffect, effect = ())] -impl MultiObserver { - fn observe_test_effect(&mut self, effect: &TestEffect) -> eyre::Result> { - self.ints.push(effect.0); - Ok(Effects::none()) - } - fn observe_another_effect(&mut self, effect: &AnotherEffect) -> eyre::Result> { - self.strings.push(effect.0.clone()); - Ok(Effects::none()) - } -} - -#[test] -fn observe_macro_handles_multiple_effect_types() { - let mut obs = MultiObserver::default(); - assert!( - Observe::::observe(&mut obs, &TestEffect(1)) - .unwrap() - .into_iter() - .next() - .is_none() - ); - assert!( - Observe::::observe(&mut obs, &AnotherEffect("hello".to_string())) - .unwrap() - .into_iter() - .next() - .is_none() - ); - assert_eq!(obs.ints, vec![1]); - assert_eq!(obs.strings, vec!["hello"]); -} - -#[derive(Default)] -struct ManualObserver { - sum: i32, -} - -impl Observe for ManualObserver { - type Effect = (); - type Error = eyre::Report; - - fn observe(&mut self, effect: &TestEffect) -> Result, Self::Error> { - self.sum = effect.0; - Ok(Effects::none()) - } -} - -#[test] -fn manual_observe_impl_uses_result_effects_signature() { - let mut obs = ManualObserver::default(); - let effects = Observe::::observe(&mut obs, &TestEffect(42)).unwrap(); - assert!(effects.into_iter().next().is_none()); - assert_eq!(obs.sum, 42); -} - -#[derive(Default)] -struct FollowUpObserver { - seen: Vec, -} - -#[observe(TestEffect, effect = AnotherEffect)] -impl FollowUpObserver { - fn observe_test_effect(&mut self, effect: &TestEffect) -> eyre::Result> { - self.seen.push(effect.0); - Ok(Effects::one(AnotherEffect(format!("echo:{:?}", effect.0)))) - } -} - -#[test] -fn observe_macro_uses_explicit_effect_type_for_follow_up_effects() { - let mut obs = FollowUpObserver::default(); - let follow_ups = Observe::::observe(&mut obs, &TestEffect(7)).unwrap(); - assert_eq!(obs.seen, vec![7]); - let effect = follow_ups.into_iter().next().unwrap(); - assert_eq!(effect.0, "echo:7"); -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum CompositeEffect { - Int(i32), - Text(String), -} - -impl From for CompositeEffect { - fn from(value: TestEffect) -> Self { - Self::Int(value.0) - } -} - -impl From for CompositeEffect { - fn from(value: AnotherEffect) -> Self { - Self::Text(value.0) - } -} - -#[derive(Default)] -struct ExplicitCompositeObserver { - seen: Vec, -} - -#[observe(TestEffect, effect = CompositeEffect)] -impl ExplicitCompositeObserver { - fn observe_test_effect_first( - &mut self, - effect: &TestEffect, - ) -> eyre::Result> { - self.seen.push(effect.0); - Ok(Effects::one(TestEffect(effect.0 + 1))) - } - - fn observe_test_effect_second( - &mut self, - effect: &TestEffect, - ) -> eyre::Result> { - self.seen.push(effect.0 * 10); - Ok(Effects::one(AnotherEffect(format!("echo:{}", effect.0)))) - } -} - -#[test] -fn observe_macro_lifts_local_effects_into_explicit_composite_effect() { - let mut obs = ExplicitCompositeObserver::default(); - let follow_ups = Observe::::observe(&mut obs, &TestEffect(7)).unwrap(); - - assert_eq!(obs.seen, vec![7, 70]); - assert_eq!( - follow_ups.into_iter().collect::>(), - vec![ - CompositeEffect::Int(8), - CompositeEffect::Text("echo:7".to_string()), - ] - ); -} - -#[derive(Default)] -struct MultiEventExplicitObserver { - ints: Vec, - strings: Vec, -} - -#[observe(TestEffect, AnotherEffect, effect = CompositeEffect)] -impl MultiEventExplicitObserver { - fn observe_test_effect( - &mut self, - effect: &TestEffect, - ) -> eyre::Result> { - self.ints.push(effect.0); - Ok(Effects::one(CompositeEffect::Int(effect.0))) - } - - fn observe_another_effect( - &mut self, - effect: &AnotherEffect, - ) -> eyre::Result> { - self.strings.push(effect.0.clone()); - Ok(Effects::one(CompositeEffect::Text(effect.0.clone()))) - } -} - -#[test] -fn observe_macro_supports_explicit_composite_effect_type() { - let mut obs = MultiEventExplicitObserver::default(); - - assert_eq!( - Observe::::observe(&mut obs, &TestEffect(3)) - .unwrap() - .into_iter() - .collect::>(), - vec![CompositeEffect::Int(3)] - ); - assert_eq!( - Observe::::observe(&mut obs, &AnotherEffect("hello".to_string())) - .unwrap() - .into_iter() - .collect::>(), - vec![CompositeEffect::Text("hello".to_string())] - ); - - assert_eq!(obs.ints, vec![3]); - assert_eq!(obs.strings, vec!["hello"]); -} - -fn write_temp_file(path: &Path, contents: &str) { - fs::create_dir_all(path.parent().unwrap()).unwrap(); - fs::write(path, contents).unwrap(); -} - -fn temp_crate_dir(name: &str) -> PathBuf { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - std::env::temp_dir().join(format!( - "vihaco-observe-macro-{}-{}-{}", - name, - std::process::id(), - unique - )) -} - -struct TempCrateDir(PathBuf); - -impl TempCrateDir { - fn new(name: &str) -> Self { - Self(temp_crate_dir(name)) - } - - fn path(&self) -> &Path { - &self.0 - } -} - -impl Drop for TempCrateDir { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } -} - -#[test] -fn observe_macro_requires_explicit_effect_for_composite_observers() { - let dir = TempCrateDir::new("missing-effect"); - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let vihaco_path = manifest_dir.canonicalize().unwrap(); - - write_temp_file( - &dir.path().join("Cargo.toml"), - &format!( - "[package]\nname = \"missing-effect\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nvihaco = {{ path = {:?} }}\n", - vihaco_path - ), - ); - write_temp_file( - &dir.path().join("src/lib.rs"), - r#" -use vihaco::{Effects, observe}; - -#[derive(Clone)] -struct TestEffect(i32); - -#[derive(Clone)] -struct AnotherEffect(String); - -#[derive(Default)] -struct MissingEffectObserver; - -#[observe(TestEffect, AnotherEffect)] -impl MissingEffectObserver { - fn observe_test_effect( - &mut self, - effect: &TestEffect, - ) -> std::result::Result, std::convert::Infallible> { - let _ = effect.0; - Ok(Effects::none()) - } - - fn observe_another_effect( - &mut self, - effect: &AnotherEffect, - ) -> std::result::Result, std::convert::Infallible> { - let _ = &effect.0; - Ok(Effects::none()) - } -} -"#, - ); - - let output = Command::new("cargo") - .arg("check") - .arg("--offline") - .arg("--manifest-path") - .arg(dir.path().join("Cargo.toml")) - .current_dir(dir.path()) - .output() - .unwrap(); - - assert!(!output.status.success(), "expected compile failure"); - - let stderr = String::from_utf8_lossy(&output.stderr); - assert!( - stderr.contains( - "generated #[observe] impls that compose multiple observed events, multiple handlers, or typed follow-up effects must declare `effect = ...`" - ), - "unexpected stderr:\n{stderr}" - ); -} diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index 4386a746..e1bac194 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -2,32 +2,35 @@ // SPDX-License-Identifier: MIT use eyre::Result; -use vihaco::{Effects, GeneratedComponent, Instruction, Observe, component, composite, observe}; +use vihaco::{Effects, Execute, Execution, Instruction, Observe, StepResult, composite}; mod test_root { pub use ::vihaco::*; } #[derive(Debug, Clone, Instruction)] -enum TestInstruction { +pub enum TestInstruction { Run, } struct TestMessage; - struct TestEffect; - struct TestComponent; -#[component(instruction = TestInstruction, message = TestMessage, effect = TestEffect)] -#[vihaco(crate = crate::test_root)] -impl TestComponent { +impl Execute for TestComponent { + type Message = TestMessage; + type Effect = TestEffect; + type Fault = eyre::Report; + fn execute( &mut self, - _instruction: TestInstruction, - _message: TestMessage, - ) -> Result> { - Ok(Effects::one(TestEffect)) + _instruction: &TestInstruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(TestEffect), + execution: Execution::Complete, + }) } } @@ -36,39 +39,59 @@ struct TestObserver { observed: bool, } -#[observe(TestEffect, effect = ())] -#[vihaco(crate = crate::test_root)] -impl TestObserver { - fn observe_test_effect(&mut self, _effect: &TestEffect) -> Result> { +impl Observe for TestObserver { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, _effect: &TestEffect) -> Result> { self.observed = true; Ok(Effects::none()) } } -#[composite] -#[vihaco(crate = crate::test_root)] -struct TestMachine { - #[device(0x01)] - component: TestComponent, +composite! { + #[vihaco(crate = crate::test_root)] + pub composite TestMachine { + error = eyre::Report; + + #[device(0x01)] + component: TestComponent, + observer: TestObserver, + } + + runtime_instructions { + Run(TestInstruction) => component { + message with resolve_message; + effects { + observe observer; + handle with handle_effect; + } + } + } +} + +impl TestMachine { + fn resolve_message(&mut self, _instruction: &TestInstruction) -> Result { + Ok(TestMessage) + } + + fn handle_effect(&mut self, _effect: TestEffect) -> Result<()> { + Ok(()) + } } #[test] fn runtime_macros_honor_explicit_crate_override() { - let mut component = TestComponent; - let effects = component - .execute_generated(TestInstruction::Run, TestMessage) + let mut machine = TestMachine { + component: TestComponent, + observer: TestObserver::default(), + }; + let outcome = machine + .execute_generated(&TestMachineInstruction::Run(TestInstruction::Run)) .unwrap(); - assert_eq!(effects.into_iter().count(), 1); - - let mut observer = TestObserver::default(); - Observe::::observe(&mut observer, &TestEffect) - .unwrap() - .into_iter() - .for_each(drop); - assert!(observer.observed); + assert_eq!(outcome, Execution::Complete); + assert!(machine.observer.observed); - let machine = TestMachine { component }; - let _ = &machine.component; let metadata = test_root::__private::GeneratedMachine::metadata(&machine); assert_eq!(metadata.devices[0].code, 0x01); assert_eq!(metadata.devices[0].name, "component"); diff --git a/demos/examples/demo.rs b/demos/examples/demo.rs index 43d5cfab..84159581 100644 --- a/demos/examples/demo.rs +++ b/demos/examples/demo.rs @@ -12,20 +12,45 @@ use std::collections::HashMap; use vihaco::Effects; -include!("demo/vihaco/execute.rs"); -include!("demo/vihaco/resume.rs"); -include!("demo/vihaco/supply.rs"); -include!("demo/vihaco/handle.rs"); -include!("demo/vihaco/machine_macro.rs"); -include!("demo/vihaco/route.rs"); -include!("demo/stdlib/debug_trace.rs"); -include!("demo/stdlib/clock.rs"); -include!("demo/stdlib/stack.rs"); -include!("demo/stdlib/arithmetic.rs"); -include!("demo/stdlib/channel.rs"); -include!("demo/src/cpu.rs"); -include!("demo/src/surface.rs"); -include!("demo/src/machine.rs"); +#[path = "demo/stdlib/arithmetic.rs"] +mod arithmetic; +#[path = "demo/stdlib/channel.rs"] +mod channel; +#[path = "demo/stdlib/clock.rs"] +mod clock; +#[path = "demo/src/cpu.rs"] +mod cpu; +#[path = "demo/stdlib/debug_trace.rs"] +mod debug_trace; +#[path = "demo/src/driver.rs"] +mod driver; +#[path = "demo/vihaco/execute.rs"] +mod execute; +#[path = "demo/vihaco/handle.rs"] +mod handle; +#[path = "demo/src/machine.rs"] +mod machine; +#[path = "demo/vihaco/machine_macro.rs"] +mod machine_macro; +#[path = "demo/vihaco/resume.rs"] +mod resume; +#[path = "demo/vihaco/route.rs"] +mod route; +#[path = "demo/stdlib/stack.rs"] +mod stack; +#[path = "demo/vihaco/supply.rs"] +mod supply; +#[path = "demo/src/surface.rs"] +mod surface; + +use arithmetic::ArithmeticUnit; +use channel::{ChannelEndpoint, ChannelFabric, EndpointId, SharedTransport}; +use clock::{GlobalClock, GlobalTicksPerLocalCycle}; +use cpu::{Cpu, CpuFault}; +use debug_trace::DebugTrace; +use machine::{CpuId, HeterogeneousMachine, RunOutcome}; +use stack::Stack; +use surface::{SurfaceInstruction, resolve_program}; fn main() -> Result<(), CpuFault> { // The two CPU programs, authored with symbolic channel names, then resolved to runtime form. @@ -51,7 +76,7 @@ fn main() -> Result<(), CpuFault> { operand_stack: Stack::seeded(&[3]), alu: ArithmeticUnit::new(), channel: ChannelEndpoint::new(EndpointId(0), transport_a), - debug: DebugTrace::default(), + debug: DebugTrace::new(), program: cpu_a_program, pc: 0, }; @@ -60,7 +85,7 @@ fn main() -> Result<(), CpuFault> { operand_stack: Stack::seeded(&[10, 4, 2]), alu: ArithmeticUnit::new(), channel: ChannelEndpoint::new(EndpointId(1), transport_b), - debug: DebugTrace::default(), + debug: DebugTrace::new(), program: cpu_b_program, pc: 0, }; @@ -70,7 +95,7 @@ fn main() -> Result<(), CpuFault> { transport: SharedTransport::new(fabric), ticks_per_local_cycle: HashMap::from([ (CpuId::A, GlobalTicksPerLocalCycle::new(3)?), - (CpuId::B, GlobalTicksPerLocalCycle::new(1)?), + (CpuId::B, GlobalTicksPerLocalCycle::new(2)?), ]), // CpuA has three global ticks per local tick. CpuB has one global tick per local tick. cpu_a, @@ -85,8 +110,8 @@ fn main() -> Result<(), CpuFault> { println!(" {line}"); } println!("outcome = {outcome:?}"); - println!("CpuA stack = {:?}", machine.cpu_a.operand_stack.items); - println!("CpuB stack = {:?}", machine.cpu_b.operand_stack.items); + println!("CpuA stack = {:?}", machine.cpu_a.operand_stack.view()); + println!("CpuB stack = {:?}", machine.cpu_b.operand_stack.view()); println!("CpuA debug = {:?}", machine.cpu_a.debug.records); println!("CpuB debug = {:?}", machine.cpu_b.debug.records); @@ -98,10 +123,10 @@ fn main() -> Result<(), CpuFault> { vec![ "global 0: CpuA recv parks on ChannelId(1)", "global 0: CpuB Sub", - "global 1: CpuB Mul", - "global 2: CpuB send on ChannelId(1)", - "global 3: CpuA wakes, recv 20", - "global 6: CpuA Mul", + "global 2: CpuB Mul", + "global 4: CpuB send on ChannelId(1)", + "global 6: CpuA wakes, recv 20", + "global 9: CpuA Mul", ] ); assert_eq!(machine.cpu_a.operand_stack.top(), Some(60)); @@ -113,5 +138,3 @@ fn main() -> Result<(), CpuFault> { println!("OK: heterogeneous exchange completed with 60 on CpuA, no stale continuation"); Ok(()) } - -include!("demo/src/driver.rs"); diff --git a/demos/examples/demo/src/cpu.rs b/demos/examples/demo/src/cpu.rs index 823fbb29..df823a1b 100644 --- a/demos/examples/demo/src/cpu.rs +++ b/demos/examples/demo/src/cpu.rs @@ -1,117 +1,82 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -// =========================================================================================== -// === AUTHOR: the machine declaration ======================================================= -// =========================================================================================== -// -// The reusable `Cpu` composite plus the root `HeterogeneousMachine`. The route wiring below -// (`routes`, `Route`/`Handle`/`Observe` impls, per-route resolvers, and the step match) is written -// out so the complete composite boundary is visible at the invocation site. - -// =========================================================================================== -// === GENERATED BY THE COMPOSITE MACRO ===================================================== -// =========================================================================================== -// -// This entire section is the expansion of the `Cpu` composite and its five runtime routes. It is -// written out here so the generated ownership, routing, observation, and execution boundaries are -// directly readable. - -struct Cpu { - operand_stack: Stack, - alu: ArithmeticUnit, - channel: ChannelEndpoint>, - debug: DebugTrace, - program: Vec, - pc: usize, -} - -#[derive(Debug, Clone, Copy)] -enum RuntimeInstruction { - IntegerAdd(Add), - IntegerSub(Sub), - IntegerMul(Mul), - Send(Send), - Recv(Recv), -} - -mod routes { - #[derive(Debug, Clone, Copy, Default)] - pub struct IntegerAdd; - #[derive(Debug, Clone, Copy, Default)] - pub struct IntegerSub; - #[derive(Debug, Clone, Copy, Default)] - pub struct IntegerMul; - #[derive(Debug, Clone, Copy, Default)] - pub struct Send; - #[derive(Debug, Clone, Copy, Default)] - pub struct Recv; -} - -impl Route for routes::IntegerAdd { - type Effect = ValueResult; - type Error = CpuFault; -} - -impl Route for routes::IntegerSub { - type Effect = ValueResult; - type Error = CpuFault; -} - -impl Route for routes::IntegerMul { - type Effect = ValueResult; - type Error = CpuFault; -} - -impl Handle for Cpu { - type Error = CpuFault; - - fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { - self.operand_stack.absorb(effect)?; - Ok(()) +use super::{ + arithmetic::{Add, ArithmeticUnit, Mul, Sub}, + channel::{ + ChannelEndpoint, ReceiveCompletion, ReceiveContinuation, ReceiveEffect, Recv, Send, + SendEffect, SharedTransport, + }, + clock::{ + ClockFault, ClockedComponent, GlobalTick, GlobalTicksPerLocalCycle, LocalCycles, Schedule, + TimedInstruction, + }, + debug_trace::DebugTrace, + execute::Execution, + handle::{Handle, Observe}, + resume::Resume, + stack::{Stack, StackFault}, +}; + +vihaco::composite! { + pub composite Cpu { + error = CpuFault; + + pub operand_stack: Stack, + pub alu: ArithmeticUnit, + pub channel: ChannelEndpoint>, + pub debug: DebugTrace, + pub program: Vec, + pub pc: usize, } -} -impl Handle for Cpu { - type Error = CpuFault; - - fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { - self.operand_stack.absorb(effect)?; - Ok(()) - } -} - -impl Handle for Cpu { - type Error = CpuFault; - - fn handle(&mut self, effect: ValueResult) -> Result<(), CpuFault> { - self.operand_stack.absorb(effect)?; - Ok(()) + runtime_instructions { + IntegerAdd(Add) => alu { + message from operand_stack; + effects { + observe debug; + absorb with operand_stack; + } + } + IntegerSub(Sub) => alu { + message from operand_stack; + effects { + observe debug; + absorb with operand_stack; + } + } + IntegerMul(Mul) => alu { + message from operand_stack; + effects { + observe debug; + absorb with operand_stack; + } + } + Send(Send) => channel { + message from operand_stack; + effects { + observe debug; + handle with handle_send; + } + } + Recv(Recv) => channel { + message none; + effects { + observe debug; + handle with handle_receive; + } + } } } -impl Route for routes::Send { - type Effect = SendEffect; - type Error = CpuFault; -} - -impl Handle for Cpu { - type Error = CpuFault; +pub type RuntimeInstruction = CpuInstruction; - fn handle(&mut self, effect: SendEffect) -> Result<(), CpuFault> { +impl Cpu { + fn handle_send(&mut self, effect: SendEffect) -> Result<(), CpuFault> { match effect {} } -} - -impl Route for routes::Recv { - type Effect = ReceiveEffect; - type Error = CpuFault; -} - -impl Handle, routes::Recv> for Cpu { - type Error = CpuFault; - fn handle(&mut self, effect: ReceiveEffect) -> Result<(), CpuFault> { + fn handle_receive(&mut self, effect: ReceiveEffect) -> Result<(), CpuFault> { match effect { ReceiveEffect::Received(value) => self.operand_stack.push(value), ReceiveEffect::Parked(_) => {} @@ -133,104 +98,20 @@ impl TimedInstruction for RuntimeInstruction { } impl Cpu { - fn fetch(&self) -> Option { - self.program.get(self.pc).copied() + pub fn fetch(&self) -> Option { + self.program.get(self.pc).cloned() } - fn finished(&self) -> bool { + pub fn finished(&self) -> bool { self.pc >= self.program.len() && !self.channel.is_parked() } - fn is_parked(&self) -> bool { + pub fn is_parked(&self) -> bool { self.channel.is_parked() } - // Keep the conversion explicit in generated route plumbing. It anchors the intended - // machine-level fault type and gives users a direct diagnostic when a component or observer - // is missing the corresponding `From<...> for CpuFault` conversion. For routes whose error is - // already `CpuFault`, this becomes the identity `Into` conversion, so Clippy reports it as - // useless; the suppression is limited to this generated dispatch boundary because the same - // expansion must support both heterogeneous and identity conversions. - #[allow(clippy::useless_conversion)] - fn execute_generated( - &mut self, - instruction: &RuntimeInstruction, - ) -> Result { - match instruction { - RuntimeInstruction::IntegerAdd(instruction) => { - let message = self.operand_stack.supply()?; - let result = self.alu.execute(instruction, message)?; - for effect in result.effects { - >::observe( - &mut self.debug, - &effect, - ) - .map_err(Into::::into)?; - >::handle(self, effect) - .map_err(Into::::into)?; - } - Ok(result.execution) - } - RuntimeInstruction::IntegerSub(instruction) => { - let message = self.operand_stack.supply()?; - let result = self.alu.execute(instruction, message)?; - for effect in result.effects { - >::observe( - &mut self.debug, - &effect, - ) - .map_err(Into::::into)?; - >::handle(self, effect) - .map_err(Into::::into)?; - } - Ok(result.execution) - } - RuntimeInstruction::IntegerMul(instruction) => { - let message = self.operand_stack.supply()?; - let result = self.alu.execute(instruction, message)?; - for effect in result.effects { - >::observe( - &mut self.debug, - &effect, - ) - .map_err(Into::::into)?; - >::handle(self, effect) - .map_err(Into::::into)?; - } - Ok(result.execution) - } - RuntimeInstruction::Send(instruction) => { - let message = self.operand_stack.supply()?; - let result = self.channel.execute(instruction, message)?; - for effect in result.effects { - >::observe( - &mut self.debug, - &effect, - ) - .map_err(Into::::into)?; - >::handle(self, effect) - .map_err(Into::::into)?; - } - Ok(result.execution) - } - RuntimeInstruction::Recv(instruction) => { - let result = self.channel.execute(instruction, NoMessage)?; - for effect in result.effects { - , routes::Recv>>::observe( - &mut self.debug, - &effect, - ) - .map_err(Into::::into)?; - , routes::Recv>>::handle(self, effect) - .map_err(Into::::into)?; - } - Ok(result.execution) - } - } - } - - // This resume path uses the same explicit, generated error normalization as dispatch above; - // some route instantiations reduce it to an identity conversion. + // Resume is intentionally hand-written in phase one. It reuses the generated receive route's + // observer and handler implementations while leaving continuation ownership to the CPU. #[allow(clippy::useless_conversion)] fn resume_receive_effects( &mut self, @@ -242,13 +123,18 @@ impl Cpu { value, })?; for effect in result.effects { - , routes::Recv>>::observe( + , + __VihacoCpuRoutes::__VihacoRoute_Recv, + >>::observe( &mut self.debug, &effect, ) .map_err(Into::::into)?; - , routes::Recv>>::handle(self, effect) - .map_err(Into::::into)?; + , __VihacoCpuRoutes::__VihacoRoute_Recv>>::handle( + self, effect, + ) + .map_err(Into::::into)?; } Ok(result.execution) } @@ -259,7 +145,7 @@ impl Cpu { fn complete_instruction( &mut self, global_tick: GlobalTick, - instruction: &RuntimeInstruction, + local_cycles: LocalCycles, outcome: Execution, ticks_per_local_cycle: GlobalTicksPerLocalCycle, ) -> Result>, CpuFault> { @@ -272,9 +158,7 @@ impl Cpu { } let start = self.next_boundary_at(global_tick, ticks_per_local_cycle)?; - let duration = instruction - .local_cycles() - .checked_mul(ticks_per_local_cycle)?; + let duration = local_cycles.checked_mul(ticks_per_local_cycle)?; let at = start .0 .checked_add(duration.0) @@ -295,7 +179,7 @@ impl Cpu { // =========================================================================================== #[derive(Debug, Clone, Copy)] -enum CpuEvent { +pub enum CpuEvent { RunNext, } @@ -312,13 +196,9 @@ impl ClockedComponent for Cpu { let Some(instruction) = self.fetch() else { return Ok(None); }; + let local_cycles = instruction.local_cycles(); let execution = self.execute_generated(&instruction)?; - self.complete_instruction( - global_tick, - &instruction, - execution, - ticks_per_local_cycle, - ) + self.complete_instruction(global_tick, local_cycles, execution, ticks_per_local_cycle) } fn resume( @@ -329,12 +209,8 @@ impl ClockedComponent for Cpu { ) -> Result>, CpuFault> { let outcome = self.resume_receive_effects(completion.continuation, completion.value)?; let instruction = self.fetch().ok_or(CpuFault::MissingInstruction)?; - self.complete_instruction( - global_tick, - &instruction, - outcome, - ticks_per_local_cycle, - ) + let local_cycles = instruction.local_cycles(); + self.complete_instruction(global_tick, local_cycles, outcome, ticks_per_local_cycle) } fn next_boundary_at( @@ -364,7 +240,7 @@ impl ClockedComponent for Cpu { /// Machine-level fault, with the `From` conversions generated for route plumbing. #[derive(Debug)] -enum CpuFault { +pub enum CpuFault { Stack(StackFault), Clock(ClockFault), UnknownEndpoint, diff --git a/demos/examples/demo/src/driver.rs b/demos/examples/demo/src/driver.rs index c403e090..725da6ec 100644 --- a/demos/examples/demo/src/driver.rs +++ b/demos/examples/demo/src/driver.rs @@ -3,7 +3,13 @@ #[cfg(test)] mod tests { - use super::*; + use crate::{ + arithmetic::Add, + channel::Recv, + clock::{ClockFault, GlobalTicksPerLocalCycle, LocalCycles, TimedInstruction}, + cpu::RuntimeInstruction, + surface::CHANNEL_A_TO_B, + }; #[test] fn instruction_timing_belongs_to_the_instruction() { diff --git a/demos/examples/demo/src/machine.rs b/demos/examples/demo/src/machine.rs index a8ad2044..2fdfef63 100644 --- a/demos/examples/demo/src/machine.rs +++ b/demos/examples/demo/src/machine.rs @@ -1,6 +1,13 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use super::{ + channel::{EndpointId, ReceiveCompletion, ReceiveContinuation, SharedTransport, Transport}, + clock::{ClockedComponent, GlobalClock, GlobalTick, GlobalTicksPerLocalCycle, Schedule}, + cpu::{Cpu, CpuEvent, CpuFault, RuntimeInstruction}, +}; +use std::collections::HashMap; + // =========================================================================================== // === AUTHOR: the top-level composite and its root event loop =============================== // =========================================================================================== @@ -13,7 +20,7 @@ /// Which CPU an event or waiter refers to. The reusable CPU is oblivious to this tag. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum CpuId { +pub enum CpuId { A, B, } @@ -21,7 +28,7 @@ enum CpuId { /// The machine-specific owned event sum interpreted by the root. The generic `GlobalClock` is /// parameterized over this type and never inspects it. #[derive(Debug, Clone, Copy)] -enum MachineEvent { +pub enum MachineEvent { /// Run the next instruction of the identified CPU. Step(CpuId), /// Resume a receive at the receiver's next local clock boundary. @@ -34,21 +41,21 @@ enum MachineEvent { /// How the machine terminated. #[derive(Debug, PartialEq, Eq)] -enum RunOutcome { +pub enum RunOutcome { /// Both programs finished with no lost value, stale continuation, or pending event. Completed, /// Every runnable CPU is parked and no delivery can satisfy any continuation. Deadlock, } -struct HeterogeneousMachine { - clock: GlobalClock, - transport: SharedTransport, - ticks_per_local_cycle: HashMap, - cpu_a: Cpu, - cpu_b: Cpu, +pub struct HeterogeneousMachine { + pub clock: GlobalClock, + pub transport: SharedTransport, + pub ticks_per_local_cycle: HashMap, + pub cpu_a: Cpu, + pub cpu_b: Cpu, /// A human-readable record of the deterministic global trace, asserted by the driver. - execution_trace: Vec, + pub execution_trace: Vec, } impl HeterogeneousMachine { @@ -66,10 +73,7 @@ impl HeterogeneousMachine { } } - fn ticks_per_local_cycle( - &self, - id: CpuId, - ) -> Result { + fn ticks_per_local_cycle(&self, id: CpuId) -> Result { self.ticks_per_local_cycle .get(&id) .copied() @@ -87,7 +91,7 @@ impl HeterogeneousMachine { /// selected child (the borrow of `GlobalClock` ends before the child is stepped), and inserts /// any resulting scheduling requests back into the clock. Terminates when the timeline is /// exhausted, distinguishing normal completion from deadlock. - fn run(&mut self) -> Result { + pub fn run(&mut self) -> Result { // Seed both CPUs to run their first instruction at global tick 0. self.clock .schedule_at(GlobalTick::ZERO, MachineEvent::Step(CpuId::A))?; @@ -123,29 +127,26 @@ impl HeterogeneousMachine { return Ok(()); }; + let (detail, parked_detail) = match instruction { + RuntimeInstruction::IntegerAdd(_) => ("Add".to_owned(), "Add parks".to_owned()), + RuntimeInstruction::IntegerSub(_) => ("Sub".to_owned(), "Sub parks".to_owned()), + RuntimeInstruction::IntegerMul(_) => ("Mul".to_owned(), "Mul parks".to_owned()), + RuntimeInstruction::Send(send) => ( + format!("send on {:?}", send.channel), + format!("send parks on {:?}", send.channel), + ), + RuntimeInstruction::Recv(recv) => ( + format!("recv on {:?}", recv.channel), + format!("recv parks on {:?}", recv.channel), + ), + }; + let ticks_per_local_cycle = self.ticks_per_local_cycle(id)?; - let schedule = self - .cpu_mut(id) - .step_at(tick, ticks_per_local_cycle)?; + let schedule = self.cpu_mut(id).step_at(tick, ticks_per_local_cycle)?; self.submit_schedule(id, schedule)?; let parked = self.cpu_ref(id).is_parked(); - let detail = match instruction { - RuntimeInstruction::IntegerAdd(_) if parked => "Add parks".to_owned(), - RuntimeInstruction::IntegerSub(_) if parked => "Sub parks".to_owned(), - RuntimeInstruction::IntegerMul(_) if parked => "Mul parks".to_owned(), - RuntimeInstruction::IntegerAdd(_) => "Add".to_owned(), - RuntimeInstruction::IntegerSub(_) => "Sub".to_owned(), - RuntimeInstruction::IntegerMul(_) => "Mul".to_owned(), - RuntimeInstruction::Send(send) if parked => { - format!("send parks on {:?}", send.channel) - } - RuntimeInstruction::Send(send) => format!("send on {:?}", send.channel), - RuntimeInstruction::Recv(recv) if parked => { - format!("recv parks on {:?}", recv.channel) - } - RuntimeInstruction::Recv(recv) => format!("recv on {:?}", recv.channel), - }; + let detail = if parked { parked_detail } else { detail }; self.record(tick, id, detail); Ok(()) @@ -163,16 +164,14 @@ impl HeterogeneousMachine { let ticks_per_local_cycle = self.ticks_per_local_cycle(id)?; // The child owns continuation completion, including its stack and parked state. The root // only supplies the opaque continuation input and resubmits the returned schedule. - let schedule = self - .cpu_mut(id) - .resume( - ReceiveCompletion { - continuation, - value, - }, - tick, - ticks_per_local_cycle, - )?; + let schedule = self.cpu_mut(id).resume( + ReceiveCompletion { + continuation, + value, + }, + tick, + ticks_per_local_cycle, + )?; self.submit_schedule(id, schedule)?; self.record(tick, id, format!("wakes, recv {value}")); Ok(()) diff --git a/demos/examples/demo/src/surface.rs b/demos/examples/demo/src/surface.rs index a37bcc5b..21808819 100644 --- a/demos/examples/demo/src/surface.rs +++ b/demos/examples/demo/src/surface.rs @@ -1,6 +1,12 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use super::{ + arithmetic::{Add, Mul, Sub}, + channel::{ChannelId, Recv, Send}, + cpu::RuntimeInstruction, +}; + // =========================================================================================== // === AUTHOR: surface programs and channel-name resolution ================================== // =========================================================================================== @@ -10,7 +16,7 @@ /// A surface instruction as authored, before channel names are resolved. #[derive(Debug, Clone, Copy)] -enum SurfaceInstruction { +pub enum SurfaceInstruction { Add, Sub, Mul, @@ -19,12 +25,12 @@ enum SurfaceInstruction { } /// The two directed channels wired into this machine. -const CHANNEL_A_TO_B: ChannelId = ChannelId(0); -const CHANNEL_B_TO_A: ChannelId = ChannelId(1); +pub const CHANNEL_A_TO_B: ChannelId = ChannelId(0); +pub const CHANNEL_B_TO_A: ChannelId = ChannelId(1); /// Resolve a symbolic channel name to its runtime identifier. `to_b`/`from_a` name the A->B /// channel; `to_a`/`from_b` name the B->A channel. -fn resolve_channel(name: &str) -> ChannelId { +pub fn resolve_channel(name: &str) -> ChannelId { match name { "to_b" | "from_a" => CHANNEL_A_TO_B, "to_a" | "from_b" => CHANNEL_B_TO_A, @@ -33,7 +39,7 @@ fn resolve_channel(name: &str) -> ChannelId { } /// Lower a whole surface program to runtime instructions, resolving channel names along the way. -fn resolve_program(surface: &[SurfaceInstruction]) -> Vec { +pub fn resolve_program(surface: &[SurfaceInstruction]) -> Vec { surface .iter() .map(|instruction| match *instruction { diff --git a/demos/examples/demo/stdlib/arithmetic.rs b/demos/examples/demo/stdlib/arithmetic.rs index cde02076..72254c86 100644 --- a/demos/examples/demo/stdlib/arithmetic.rs +++ b/demos/examples/demo/stdlib/arithmetic.rs @@ -1,51 +1,39 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// A reusable, stateless `i64` arithmetic component implementing `add`, `sub`, and `mul`. It is pure -/// behavior: it does not know which CPU contains it, which stack supplied the values, or how long an -/// operation takes. Because it carries no timing, the same component works unchanged whether -/// execution is driven by a clock or by real time. -struct ArithmeticUnit; +use crate::stack::{Stack, StackFault}; -impl ArithmeticUnit { - fn new() -> Self { - Self - } -} +use super::{ + Effects, + execute::{Execute, Execution, StepResult}, + handle::Absorb, + supply::Supply, +}; -/* -component! { - component ArithmeticUnit; +vihaco::component! { + component ArithmeticUnit {} - #[namespace("arith")] - instruction Arithmetic { - #[pattern = "'add"] + instruction { + #[derive(Debug, Clone, Copy)] Add, - #[pattern = "'sub"] + #[derive(Debug, Clone, Copy)] Sub, - #[pattern = "'mul"] - Mul, + #[derive(Debug, Clone, Copy)] + Mul } } -*/ -enum Arithmetic { - Add(Add), - Sub(Sub), - Mul(Mul), -} +pub use arithmetic_unit::ArithmeticUnit; +pub use arithmetic_unit::instruction::{Add, Mul, Sub}; -/// The three arithmetic runtime instructions. Each is a distinct payload type so `Execute` can -/// select the operation, while the surrounding route ZST selects where the result lands. -#[derive(Debug, Clone, Copy)] -struct Add; -#[derive(Debug, Clone, Copy)] -struct Sub; -#[derive(Debug, Clone, Copy)] -struct Mul; +impl ArithmeticUnit { + pub fn new() -> Self { + Self {} + } +} /// The message the composite resolves for an arithmetic op (its two operands). -struct BinaryOperands { +pub struct BinaryOperands { lhs: i64, rhs: i64, } @@ -53,7 +41,7 @@ struct BinaryOperands { /// The semantic effect arithmetic produces. It carries a value and names no destination, which is /// why several routes can share it. #[derive(Debug)] -struct ValueResult(i64); +pub struct ValueResult(i64); impl Execute for ArithmeticUnit { type Message = BinaryOperands; diff --git a/demos/examples/demo/stdlib/channel.rs b/demos/examples/demo/stdlib/channel.rs index 573a30db..38ffecde 100644 --- a/demos/examples/demo/stdlib/channel.rs +++ b/demos/examples/demo/stdlib/channel.rs @@ -1,6 +1,12 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use super::{ + Effects, + execute::{Execute, Execution, NoMessage, StepResult}, + resume::Resume, +}; + use std::cell::RefCell; use std::collections::VecDeque; use std::marker::PhantomData; @@ -9,41 +15,31 @@ use std::rc::Rc; /// A library-defined runtime channel identifier. Surface channel names resolve to this before /// execution; the CPU and arithmetic components never see the symbolic name. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ChannelId(usize); - -#[derive(Debug, Clone, Copy)] -struct Send { - channel: ChannelId, -} - -#[derive(Debug, Clone, Copy)] -struct Recv { - channel: ChannelId, -} +pub struct ChannelId(pub usize); /// Identity used by the transport to return a completion to the endpoint that parked. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct EndpointId(u8); +pub struct EndpointId(pub u8); #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ReceiveContinuation { - endpoint: EndpointId, - channel: ChannelId, +pub struct ReceiveContinuation { + pub endpoint: EndpointId, + pub channel: ChannelId, } -struct ReceiveCompletion { - continuation: ReceiveContinuation, - value: M, +pub struct ReceiveCompletion { + pub continuation: ReceiveContinuation, + pub value: M, } -enum ReceivePoll { +pub enum ReceivePoll { Ready(M), Parked(ReceiveContinuation), } /// A transport is a capability supplied to a channel endpoint. It knows nothing about CPUs or /// composite containment. Wakeups are owned by the transport and polled by the runtime root. -trait Transport { +pub trait Transport { type Fault; fn send(&mut self, channel: ChannelId, value: M) -> Result<(), Self::Fault>; @@ -59,14 +55,14 @@ trait Transport { /// A reusable shared communication component. This demo uses immediate delivery; another /// transport can implement latency or topology policy without changing `ChannelEndpoint` or `Cpu`. -struct ChannelFabric { +pub struct ChannelFabric { queues: Vec>, waiters: Vec>, wakeups: VecDeque<(ReceiveContinuation, M)>, } impl ChannelFabric { - fn with_channels(count: usize) -> Self { + pub fn with_channels(count: usize) -> Self { Self { queues: (0..count).map(|_| VecDeque::new()).collect(), waiters: vec![None; count], @@ -113,10 +109,10 @@ impl Transport for ChannelFabric { /// The capability copied into each endpoint. Cloning it shares the transport, not endpoint state. #[derive(Clone)] -struct SharedTransport(Rc>>); +pub struct SharedTransport(Rc>>); impl SharedTransport { - fn new(fabric: Rc>>) -> Self { + pub fn new(fabric: Rc>>) -> Self { Self(fabric) } } @@ -142,25 +138,35 @@ impl Transport for SharedTransport { } #[derive(Debug)] -enum SendEffect {} +pub enum SendEffect {} #[derive(Debug)] -enum ReceiveEffect { +pub enum ReceiveEffect { Received(M), Parked(ReceiveContinuation), } -/// The component on which the communication instructions execute. Its transport is supplied at -/// construction, so its behavior is independent of where the CPU is placed in a machine. -struct ChannelEndpoint { - id: EndpointId, - transport: T, - parked: Option, - _message: PhantomData M>, +vihaco::component! { + component ChannelEndpoint { + id: EndpointId, + transport: T, + parked: Option, + _message: PhantomData M>, + } + + instruction { + #[derive(Debug, Clone, Copy)] + Send { channel: ChannelId }, + #[derive(Debug, Clone, Copy)] + Recv { channel: ChannelId } + } } +pub use channel_endpoint::ChannelEndpoint; +pub use channel_endpoint::instruction::{Recv, Send}; + impl ChannelEndpoint { - fn new(id: EndpointId, transport: T) -> Self { + pub fn new(id: EndpointId, transport: T) -> Self { Self { id, transport, @@ -169,7 +175,7 @@ impl ChannelEndpoint { } } - fn is_parked(&self) -> bool { + pub fn is_parked(&self) -> bool { self.parked.is_some() } } @@ -243,7 +249,15 @@ impl Resume> for ChannelEndpoint { #[cfg(test)] mod channel_tests { - use super::*; + use super::super::{ + execute::{Execute, NoMessage}, + resume::Resume, + }; + use super::{ + ChannelEndpoint, ChannelFabric, ChannelId, EndpointId, ReceiveCompletion, ReceiveEffect, + Recv, Send, SharedTransport, Transport, + }; + use std::{cell::RefCell, rc::Rc}; #[test] fn queued_values_are_fifo() { @@ -251,14 +265,29 @@ mod channel_tests { let mut endpoint = ChannelEndpoint::new(EndpointId(0), SharedTransport::new(fabric)); endpoint - .execute(&Send { channel: ChannelId(0) }, 10) + .execute( + &Send { + channel: ChannelId(0), + }, + 10, + ) .unwrap(); endpoint - .execute(&Send { channel: ChannelId(0) }, 20) + .execute( + &Send { + channel: ChannelId(0), + }, + 20, + ) .unwrap(); let first = endpoint - .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .execute( + &Recv { + channel: ChannelId(0), + }, + NoMessage, + ) .unwrap() .effects .into_iter() @@ -266,7 +295,12 @@ mod channel_tests { assert!(matches!(first, Some(ReceiveEffect::Received(10)))); let second = endpoint - .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .execute( + &Recv { + channel: ChannelId(0), + }, + NoMessage, + ) .unwrap() .effects .into_iter() @@ -277,11 +311,17 @@ mod channel_tests { #[test] fn send_execute_wakes_a_parked_recv_execute() { let fabric = Rc::new(RefCell::new(ChannelFabric::with_channels(1))); - let mut receiver = ChannelEndpoint::new(EndpointId(1), SharedTransport::new(fabric.clone())); + let mut receiver = + ChannelEndpoint::new(EndpointId(1), SharedTransport::new(fabric.clone())); let mut sender = ChannelEndpoint::new(EndpointId(0), SharedTransport::new(fabric.clone())); let parked = receiver - .execute(&Recv { channel: ChannelId(0) }, NoMessage) + .execute( + &Recv { + channel: ChannelId(0), + }, + NoMessage, + ) .unwrap() .effects .into_iter() @@ -290,12 +330,20 @@ mod channel_tests { assert!(receiver.is_parked()); sender - .execute(&Send { channel: ChannelId(0) }, 42) + .execute( + &Send { + channel: ChannelId(0), + }, + 42, + ) .unwrap(); let (continuation, value) = receiver.transport.take_wakeup().unwrap(); let effects = receiver - .resume(ReceiveCompletion { continuation, value }) + .resume(ReceiveCompletion { + continuation, + value, + }) .unwrap() .effects; assert!(matches!( diff --git a/demos/examples/demo/stdlib/clock.rs b/demos/examples/demo/stdlib/clock.rs index bc14e438..5af15c25 100644 --- a/demos/examples/demo/stdlib/clock.rs +++ b/demos/examples/demo/stdlib/clock.rs @@ -27,27 +27,27 @@ use std::collections::BinaryHeap; /// `(tick, seq)`; the sequence number gives stable ordering to events scheduled for the same global /// tick. Host execution time never contributes to modeled duration. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct GlobalTick(u64); +pub struct GlobalTick(pub u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct GlobalDuration(u64); +pub struct GlobalDuration(pub u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct LocalCycles(u64); +pub struct LocalCycles(pub u64); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -struct GlobalTicksPerLocalCycle(u64); +pub struct GlobalTicksPerLocalCycle(pub u64); /// Runtime instructions provide the local duration of their own operation. -trait TimedInstruction { +pub trait TimedInstruction { fn local_cycles(&self) -> LocalCycles; } /// Owned scheduling work returned by a clocked component. The parent adds any child identity /// before submitting the request to its root `GlobalClock`. -struct Schedule { - at: GlobalTick, - event: E, +pub struct Schedule { + pub at: GlobalTick, + pub event: E, } /// Generic boundary for a component that participates in a global event loop. @@ -55,7 +55,7 @@ struct Schedule { /// The trait shares only clock vocabulary with `GlobalClock`: ticks, instruction timing, and /// owned scheduling requests. It does not depend on a particular clock implementation or root /// event enum. Components supply their own instruction, event, completion, and fault types. -trait ClockedComponent { +pub trait ClockedComponent { type Event; type Completion; type Fault; @@ -79,9 +79,9 @@ trait ClockedComponent { } impl GlobalTick { - const ZERO: Self = Self(0); + pub const ZERO: Self = Self(0); - fn checked_add(self, duration: GlobalDuration) -> Result { + pub fn checked_add(self, duration: GlobalDuration) -> Result { self.0 .checked_add(duration.0) .map(Self) @@ -90,16 +90,19 @@ impl GlobalTick { } impl LocalCycles { - const ONE: Self = Self(1); + pub const ONE: Self = Self(1); - fn checked_add(self, other: Self) -> Result { + pub fn checked_add(self, other: Self) -> Result { self.0 .checked_add(other.0) .map(Self) .ok_or(ClockFault::LocalCycleOverflow) } - fn checked_mul(self, ratio: GlobalTicksPerLocalCycle) -> Result { + pub fn checked_mul( + self, + ratio: GlobalTicksPerLocalCycle, + ) -> Result { self.0 .checked_mul(ratio.0) .map(GlobalDuration) @@ -108,7 +111,7 @@ impl LocalCycles { } impl GlobalTicksPerLocalCycle { - fn new(value: u64) -> Result { + pub fn new(value: u64) -> Result { (value != 0) .then_some(Self(value)) .ok_or(ClockFault::ZeroTickRatio) @@ -116,7 +119,7 @@ impl GlobalTicksPerLocalCycle { } #[derive(Debug, PartialEq, Eq)] -enum ClockFault { +pub enum ClockFault { ZeroTickRatio, LocalCycleOverflow, DurationOverflow, @@ -125,13 +128,13 @@ enum ClockFault { SchedulingInPast, } -struct GlobalClock { +pub struct GlobalClock { now: GlobalTick, seq: u64, pending: BinaryHeap>, } -struct Scheduled { +pub struct Scheduled { tick: GlobalTick, seq: u64, event: E, @@ -163,7 +166,7 @@ impl PartialEq for Scheduled { impl Eq for Scheduled {} impl GlobalClock { - fn new() -> Self { + pub fn new() -> Self { Self { now: GlobalTick::ZERO, seq: 0, @@ -172,7 +175,7 @@ impl GlobalClock { } /// Insert owned scheduling work at an absolute global tick. - fn schedule_at(&mut self, tick: GlobalTick, event: E) -> Result<(), ClockFault> { + pub fn schedule_at(&mut self, tick: GlobalTick, event: E) -> Result<(), ClockFault> { if tick < self.now { return Err(ClockFault::SchedulingInPast); } @@ -186,31 +189,31 @@ impl GlobalClock { } /// Convert child-local relative work into an absolute global tick. - fn schedule_after(&mut self, after: GlobalDuration, event: E) -> Result<(), ClockFault> { + pub fn schedule_after(&mut self, after: GlobalDuration, event: E) -> Result<(), ClockFault> { self.schedule_at(self.now.checked_add(after)?, event) } /// Remove the earliest owned event by `(tick, seq)`, advancing `now` to it. Returns the event /// and its tick, or `None` when the timeline is exhausted. - fn pop_earliest(&mut self) -> Option<(GlobalTick, E)> { + pub fn pop_earliest(&mut self) -> Option<(GlobalTick, E)> { let Scheduled { tick, event, .. } = self.pending.pop()?; // Global time is monotonic: `now` only ever advances to the dispatched event's tick. self.now = tick; Some((tick, event)) } - fn now(&self) -> GlobalTick { + pub fn now(&self) -> GlobalTick { self.now } - fn is_empty(&self) -> bool { + pub fn is_empty(&self) -> bool { self.pending.is_empty() } } #[cfg(test)] mod clock_tests { - use super::*; + use super::{GlobalClock, GlobalTick}; #[test] fn heap_returns_events_in_timeline_order() { diff --git a/demos/examples/demo/stdlib/debug_trace.rs b/demos/examples/demo/stdlib/debug_trace.rs index e1a4ea22..6a1e420b 100644 --- a/demos/examples/demo/stdlib/debug_trace.rs +++ b/demos/examples/demo/stdlib/debug_trace.rs @@ -1,30 +1,43 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// A generic debug component that records every observed effect with its route's type name. -#[derive(Debug, Default)] -struct DebugTrace { - records: Vec, +use super::{Effects, handle::Observe}; + +vihaco::component! { + component DebugTrace { + pub records: Vec, + } +} + +pub use debug_trace::DebugTrace; + +impl debug_trace::DebugTrace { + pub fn new() -> Self { + Self { + records: Vec::new(), + } + } } #[derive(Debug)] -struct DebugRecord { +pub struct DebugRecord { route: &'static str, effect: String, } -impl Observe for DebugTrace +impl Observe for debug_trace::DebugTrace where E: std::fmt::Debug, - R: Route, + R: 'static, { - type Error = R::Error; + type Effect = (); + type Error = std::convert::Infallible; - fn observe(&mut self, effect: &E) -> Result<(), Self::Error> { + fn observe(&mut self, effect: &E) -> Result, Self::Error> { self.records.push(DebugRecord { route: std::any::type_name::(), effect: format!("{effect:?}"), }); - Ok(()) + Ok(Effects::none()) } } diff --git a/demos/examples/demo/stdlib/stack.rs b/demos/examples/demo/stdlib/stack.rs index eda9c550..2ce908e8 100644 --- a/demos/examples/demo/stdlib/stack.rs +++ b/demos/examples/demo/stdlib/stack.rs @@ -1,34 +1,48 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// A reusable operand-stack component with invariant-preserving operations. -struct Stack { - items: Vec, +use super::supply::Supply; + +vihaco::component! { + component Stack { + items: Vec, + } + + instruction { + Push(i64), + Pop, + } } +pub use stack::Stack; + impl Stack { - fn new() -> Self { + pub fn new() -> Self { Self { items: Vec::new() } } /// Load initial operands with the rightmost value treated as the top of the stack. - fn seeded(values: &[i64]) -> Self { + pub fn seeded(values: &[i64]) -> Self { Self { items: values.to_vec(), } } - fn push(&mut self, value: i64) { + pub fn push(&mut self, value: i64) { self.items.push(value); } - fn pop(&mut self) -> Result { + pub fn pop(&mut self) -> Result { self.items.pop().ok_or(StackFault::Underflow) } - fn top(&self) -> Option { + pub fn top(&self) -> Option { self.items.last().copied() } + + pub fn view(&self) -> &[i64] { + &self.items + } } impl Supply for Stack { @@ -40,6 +54,6 @@ impl Supply for Stack { } #[derive(Debug)] -enum StackFault { +pub enum StackFault { Underflow, } diff --git a/demos/examples/demo/vihaco/execute.rs b/demos/examples/demo/vihaco/execute.rs index 848ca59d..0104aabb 100644 --- a/demos/examples/demo/vihaco/execute.rs +++ b/demos/examples/demo/vihaco/execute.rs @@ -1,36 +1,4 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// Marker message for instructions whose execution does not require a runtime-supplied message. -#[derive(Debug, Clone, Copy, Default)] -struct NoMessage; - -/// Outcome of one instruction step. This is independent of any timing model. It answers whether -/// the parent may advance the program counter or must keep the composite parked. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Execution { - /// The step resolved; the parent may advance to the next instruction. - Complete, - /// The step is unresolved; the parent must wait for a completion. - Parked, -} - -/// The standardized result of starting or resuming one instruction route. Effects are handled -/// independently from the route's completion state. -struct StepResult { - effects: Effects, - execution: Execution, -} - -/// A component executes one fully-resolved runtime instruction against its own state. -trait Execute { - type Message; - type Effect; - type Fault; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result, Self::Fault>; -} +pub use vihaco::{Execute, Execution, NoMessage, StepResult}; diff --git a/demos/examples/demo/vihaco/handle.rs b/demos/examples/demo/vihaco/handle.rs index 60061bf2..12e24d5b 100644 --- a/demos/examples/demo/vihaco/handle.rs +++ b/demos/examples/demo/vihaco/handle.rs @@ -1,25 +1,4 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// A reusable, machine-agnostic capability: this component knows how to swallow this effect. -trait Absorb { - type Fault; - - fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; -} - -/// A non-consuming effect observer. Observers borrow effects before their semantic handler -/// consumes them and do not determine the effect's destination. -trait Observe { - type Error; - - fn observe(&mut self, effect: &Effect) -> Result<(), Self::Error>; -} - -/// Effect handling, disambiguated by `Route`. The macro normally generates implementations that -/// forward to `Absorb`. -trait Handle { - type Error; - - fn handle(&mut self, effect: Effect) -> Result<(), Self::Error>; -} +pub use vihaco::{Absorb, Handle, Observe}; diff --git a/demos/examples/demo/vihaco/resume.rs b/demos/examples/demo/vihaco/resume.rs index 743589e5..d5e260a1 100644 --- a/demos/examples/demo/vihaco/resume.rs +++ b/demos/examples/demo/vihaco/resume.rs @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use super::execute::StepResult; + /// A component resumes a previously parked operation from an owned completion. -trait Resume { +pub trait Resume { type Effect; type Fault; diff --git a/demos/examples/demo/vihaco/route.rs b/demos/examples/demo/vihaco/route.rs index 6b026c18..5207813b 100644 --- a/demos/examples/demo/vihaco/route.rs +++ b/demos/examples/demo/vihaco/route.rs @@ -12,7 +12,7 @@ /// /// The composite machinery generates one marker and one implementation for every selected route. /// Users provide the component operations and handlers; they do not implement this trait. -trait Route { +pub trait Route { /// Effect produced by the component on this route and passed to its observers and handlers. /// /// The association lets generated dispatch name the route once and derive the effect type diff --git a/demos/examples/demo/vihaco/supply.rs b/demos/examples/demo/vihaco/supply.rs index 4307145a..cf81cb55 100644 --- a/demos/examples/demo/vihaco/supply.rs +++ b/demos/examples/demo/vihaco/supply.rs @@ -1,9 +1,4 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -/// The dual of `Absorb`: this component knows how to hand out this message type. -trait Supply { - type Fault; - - fn supply(&mut self) -> Result; -} +pub use vihaco::Supply; diff --git a/docs/examples/counter.rs b/docs/examples/counter.rs index 13443c34..265c344d 100644 --- a/docs/examples/counter.rs +++ b/docs/examples/counter.rs @@ -1,5 +1,5 @@ use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; +use vihaco::{Effects, Execute, Execution, Instruction, Message, StepResult}; /// Bytecode-visible operations. Each variant becomes an opcode; tuple /// fields become the payload bytes that follow it. @@ -11,9 +11,11 @@ pub enum CounterInst { /// Resolved execution input — supplied by the runtime, not encoded in /// the instruction stream. -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone)] pub struct Prefix(pub String); +impl Message for Prefix {} + /// A value the component returns for the runtime or observers to consume. #[derive(Debug, Clone, PartialEq)] pub struct Line(pub String); @@ -23,10 +25,8 @@ pub struct Counter { value: i64, } -// One `execute` per component: (instruction, message) in, effects out. -#[component(instruction = CounterInst, message = Prefix, effect = Line)] impl Counter { - fn execute(&mut self, inst: CounterInst, msg: Prefix) -> Result> { + fn execute_instruction(&mut self, inst: &CounterInst, msg: Prefix) -> Result> { match inst { CounterInst::Add(v) => { self.value += v; @@ -36,3 +36,20 @@ impl Counter { } } } + +impl Execute for Counter { + type Message = Prefix; + type Effect = Line; + type Fault = eyre::Report; + + fn execute( + &mut self, + inst: &CounterInst, + msg: Prefix, + ) -> Result> { + Ok(StepResult { + effects: self.execute_instruction(inst, msg)?, + execution: Execution::Complete, + }) + } +} diff --git a/docs/examples/observe.rs b/docs/examples/observe.rs index 882f9b04..14c30974 100644 --- a/docs/examples/observe.rs +++ b/docs/examples/observe.rs @@ -1,5 +1,5 @@ use eyre::Result; -use vihaco::{Effects, observe}; +use vihaco::{Effects, Observe}; #[derive(Debug, Clone)] pub struct Line(pub String); @@ -11,11 +11,11 @@ pub struct Collector { lines: Vec, } -// `#[observe(T)]` generates `Observe`; the handler is named -// `observe_` and may return follow-up effects. -#[observe(Line)] -impl Collector { - fn observe_line(&mut self, effect: &Line) -> Result> { +impl Observe for Collector { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &Line) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } diff --git a/docs/examples/quickstart.rs b/docs/examples/quickstart.rs index 5be57959..6861659e 100644 --- a/docs/examples/quickstart.rs +++ b/docs/examples/quickstart.rs @@ -1,6 +1,6 @@ use eyre::Result; use vihaco::{ - Effects, GeneratedComponent, Instruction, Message, component, expect_exactly_one_effect, + Effects, Execute, Execution, Instruction, Message, StepResult, expect_exactly_one_effect, }; #[derive(Debug, Clone, Instruction)] @@ -9,9 +9,11 @@ pub enum CounterInst { Print, } -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone)] pub struct Prefix(pub String); +impl Message for Prefix {} + #[derive(Debug, Clone, PartialEq)] pub struct Line(pub String); @@ -20,9 +22,8 @@ pub struct Counter { value: i64, } -#[component(instruction = CounterInst, message = Prefix, effect = Line)] impl Counter { - fn execute(&mut self, inst: CounterInst, msg: Prefix) -> Result> { + fn execute_instruction(&mut self, inst: &CounterInst, msg: Prefix) -> Result> { match inst { CounterInst::Add(v) => { self.value += v; @@ -33,15 +34,37 @@ impl Counter { } } +impl Execute for Counter { + type Message = Prefix; + type Effect = Line; + type Fault = eyre::Report; + + fn execute( + &mut self, + inst: &CounterInst, + msg: Prefix, + ) -> Result> { + Ok(StepResult { + effects: self.execute_instruction(inst, msg)?, + execution: Execution::Complete, + }) + } +} + fn main() -> Result<()> { let mut counter = Counter::default(); // `Add` ignores its message and returns no effects. - counter.execute_generated(CounterInst::Add(2), Prefix(String::new()))?; - counter.execute_generated(CounterInst::Add(3), Prefix(String::new()))?; + Execute::execute(&mut counter, &CounterInst::Add(2), Prefix(String::new()))?; + Execute::execute(&mut counter, &CounterInst::Add(3), Prefix(String::new()))?; // `Print` returns exactly one `Line` effect. - let effects = counter.execute_generated(CounterInst::Print, Prefix("total = ".into()))?; + let effects = Execute::execute( + &mut counter, + &CounterInst::Print, + Prefix("total = ".into()), + )? + .effects; let line = expect_exactly_one_effect(effects)?; assert_eq!(line, Line("total = 5".into())); Ok(()) diff --git a/docs/src/pages/guide/components.md b/docs/src/pages/guide/components.md index 220045ca..b45f9bce 100644 --- a/docs/src/pages/guide/components.md +++ b/docs/src/pages/guide/components.md @@ -46,9 +46,11 @@ pub enum CounterInst { Print, } -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone)] pub struct PrintPrefix(pub String); +impl Message for PrintPrefix {} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct StdoutEffect(pub String); @@ -62,7 +64,7 @@ pub struct Counter { Component execution lives on an impl block annotated with `#[component(...)]`. -```rust +```rust ignore # use eyre::Result; # use vihaco::{Effects, Instruction, Message, component}; # #[derive(Debug, Clone, Instruction)] @@ -114,7 +116,7 @@ It is useful to keep the data flow straight: Use `message = ()` when the component can execute directly from its instruction and local state. -```rust +```rust ignore use eyre::Result; use vihaco::{Effects, Instruction, component}; @@ -168,7 +170,7 @@ Component execution depends only on explicit inputs and returned effects. By default, `execute(...)` returns `Result>`. When a component needs to return a real effect, use the `effect` parameter: -```rust +```rust ignore use vihaco::{Effects, Instruction, Message, component}; use vihaco_cpu::StepOutcome; diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index b79db467..a5be9559 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -26,7 +26,7 @@ Assume you already have: ```rust use eyre::Result; -use vihaco::{Effects, observe}; +use vihaco::{Effects, Observe}; #[derive(Debug, Clone)] pub struct StdoutEffect(pub String); @@ -36,9 +36,11 @@ pub struct StdoutCollector { lines: Vec, } -#[observe(StdoutEffect)] -impl StdoutCollector { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +impl Observe for StdoutCollector { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } @@ -47,9 +49,9 @@ impl StdoutCollector { Now you can compose a runtime root: -```rust +```rust ignore # use eyre::Result; -# use vihaco::{Effects, Instruction, component, observe}; +# use vihaco::{Effects, Instruction, Observe, component}; # #[derive(Debug, Clone, Instruction)] # pub enum CounterInst { Print } # #[derive(Debug, Default)] @@ -62,9 +64,10 @@ Now you can compose a runtime root: # pub struct StdoutEffect(pub String); # #[derive(Debug, Default)] # pub struct StdoutCollector { lines: Vec } -# #[observe(StdoutEffect)] -# impl StdoutCollector { -# fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +# impl Observe for StdoutCollector { +# type Effect = (); +# type Error = eyre::Report; +# fn observe(&mut self, effect: &StdoutEffect) -> Result> { # self.lines.push(effect.0.clone()); # Ok(Effects::none()) # } diff --git a/docs/src/pages/guide/instructions.md b/docs/src/pages/guide/instructions.md index 89d0f009..88fc5cb0 100644 --- a/docs/src/pages/guide/instructions.md +++ b/docs/src/pages/guide/instructions.md @@ -49,7 +49,7 @@ That means the first variant gets opcode `0`, the second gets `1`, and so on. In normal component code, this instruction type is the `instruction = ...` value on the component impl: -```rust +```rust ignore use eyre::Result; use vihaco::{Instruction, component}; diff --git a/docs/src/pages/guide/messages.md b/docs/src/pages/guide/messages.md index 728cf4ae..bbf2f216 100644 --- a/docs/src/pages/guide/messages.md +++ b/docs/src/pages/guide/messages.md @@ -50,21 +50,23 @@ That keeps responsibilities clean: ## A Small Message Type -Message types are usually plain Rust types annotated with `#[derive(Message)]`. +Message types are usually plain Rust types with an explicit `Message` marker implementation. ```rust use vihaco::Message; -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone)] pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32, } + +impl Message for PlayMsg {} ``` A component can then declare that message type in its `#[component(...)]` impl: -```rust +```rust ignore use eyre::Result; use vihaco::{Effects, Instruction, Message, component}; @@ -139,7 +141,7 @@ That is the mental model to keep throughout the rest of this guide. `#[composite]` generates the device wiring (the outer instruction enum and device metadata), but message resolution is plain Rust that you write next to the composite: build the message from runtime context, then hand `(instruction, message)` to the component via the generated `execute_generated` method. -```rust +```rust ignore use eyre::Result; use vihaco::{Effects, GeneratedComponent, Instruction, Message, component, composite}; @@ -277,7 +279,7 @@ Use `message = ()` when the component can execute directly from: For example: -```rust +```rust ignore use eyre::Result; use vihaco::{Effects, Instruction, component}; @@ -310,13 +312,13 @@ When an outer composite wraps inner components, it can also wrap their message t ```rust use vihaco::Message; -#[derive(Message)] struct DemoMsg; +impl Message for DemoMsg {} -#[derive(Message)] enum CompositeMsg { Inner(DemoMsg), } +impl Message for CompositeMsg {} ``` This pattern keeps the outer component or composite boundary explicit: diff --git a/docs/src/pages/guide/observers.md b/docs/src/pages/guide/observers.md index 70cf3110..6f71e538 100644 --- a/docs/src/pages/guide/observers.md +++ b/docs/src/pages/guide/observers.md @@ -17,11 +17,11 @@ This guide explains what `#[observe]` is for and how to use it, both on standalo ## What `#[observe]` Looks Like -`#[observe(EffectType)]` goes on an impl block. It declares which delivered effect types the type handles and generates the `Observe` trait impl. +An explicit `impl Observe` declares which delivered effect types a type handles. ```rust use eyre::Result; -use vihaco::{Effects, observe}; +use vihaco::{Effects, Observe}; #[derive(Debug, Clone)] pub struct StdoutEffect(pub String); @@ -31,9 +31,11 @@ pub struct StdoutCollector { lines: Vec, } -#[observe(StdoutEffect)] -impl StdoutCollector { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +impl Observe for StdoutCollector { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } @@ -55,7 +57,7 @@ Observer handlers can also synthesize follow-up effects. If a handler returns va You can define multiple handler methods for the same effect type by adding a suffix after the base name: -```rust +```rust ignore # use eyre::Result; # use vihaco::{Effects, observe}; # #[derive(Debug, Clone)] @@ -82,7 +84,7 @@ All methods matching `observe_` or `observe__*` are call A single `#[observe]` block can handle multiple delivered effect types: -```rust +```rust ignore # use eyre::Result; # use vihaco::{Effects, observe}; # #[derive(Debug, Clone)] @@ -119,7 +121,7 @@ That keeps continuation explicit and allows each child observer to return its ow ```rust use eyre::Result; -use vihaco::{Effects, Observe, observe}; +use vihaco::{Effects, Observe}; #[derive(Debug, Clone)] pub struct ChannelFrame; @@ -156,9 +158,11 @@ pub struct Runtime { display: Display, } -#[observe(ChannelFrame, effect = RuntimeEffect)] -impl Runtime { - fn observe_channel_frame(&mut self, effect: &ChannelFrame) -> Result> { +impl Observe for Runtime { + type Effect = RuntimeEffect; + type Error = eyre::Report; + + fn observe(&mut self, effect: &ChannelFrame) -> Result> { Ok(Observe::::observe(&mut self.display, effect)?.map(Into::into)) } } @@ -188,7 +192,7 @@ The simplest use of `#[observe]` is on a type that only reacts to delivered effe ```rust # use eyre::Result; -# use vihaco::{Effects, observe}; +# use vihaco::{Effects, Observe}; # #[derive(Debug, Clone)] # pub struct StdoutEffect(pub String); #[derive(Debug, Default)] @@ -196,9 +200,11 @@ pub struct StdoutCollector { lines: Vec, } -#[observe(StdoutEffect)] -impl StdoutCollector { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +impl Observe for StdoutCollector { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } @@ -302,7 +308,7 @@ The example below shows the full picture: ```rust use eyre::Result; -use vihaco::{Effects, Instruction, Message, component, observe}; +use vihaco::{Effects, Instruction, Message, component}; #[derive(Debug, Clone, Instruction)] pub enum WaveInst { @@ -310,12 +316,14 @@ pub enum WaveInst { Play, } -#[derive(Debug, Clone, Message)] +#[derive(Debug, Clone)] pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32, } +impl Message for PlayMsg {} + #[derive(Debug, Clone, PartialEq)] pub struct StdoutEffect(pub String); @@ -329,13 +337,14 @@ pub struct ChannelSample { ### The Producing Component -```rust +```rust ignore # use eyre::Result; # use vihaco::{Effects, Instruction, Message, component}; # #[derive(Debug, Clone, Instruction)] # pub enum WaveInst { SetAmplitude(f64), Play } -# #[derive(Debug, Clone, Message)] +# #[derive(Debug, Clone)] # pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32 } +# impl Message for PlayMsg {} # #[derive(Debug, Clone, PartialEq)] # pub struct ChannelSample { pub when_ns: u64, pub channel_id: u32, pub value: f64 } #[derive(Debug, Default)] @@ -365,7 +374,7 @@ impl WaveGenerator { ```rust # use eyre::Result; -# use vihaco::{Effects, observe}; +# use vihaco::{Effects, Observe}; # #[derive(Debug, Clone)] # pub struct StdoutEffect(pub String); #[derive(Debug, Default)] @@ -373,9 +382,11 @@ pub struct StdoutCollector { lines: Vec, } -#[observe(StdoutEffect)] -impl StdoutCollector { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { +impl Observe for StdoutCollector { + type Effect = (); + type Error = eyre::Report; + + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } @@ -384,7 +395,7 @@ impl StdoutCollector { ### A Component That Also Observes -```rust +```rust ignore # use eyre::Result; # use vihaco::{Effects, Instruction, component, observe}; # #[derive(Debug, Clone, PartialEq)] diff --git a/vision/composite-surface-runtime-declaration.md b/vision/composite-surface-runtime-declaration.md new file mode 100644 index 00000000..206aea97 --- /dev/null +++ b/vision/composite-surface-runtime-declaration.md @@ -0,0 +1,167 @@ +# Composite Surface and Runtime Declaration + +An executable composite declares both the SST-facing instruction set and the +runtime products that execute on its components. The surface instruction is +parsed by generated syntax machinery, lowered into the composite's runtime +instruction sum, and then dispatched through a typed `Execute` route. + +```rust +vihaco::composite! { + pub composite ControlMachine { + error = ControlMachineFault; + + #[loadable] + pub loader: ProgramImage< + RuntimeInstruction, + NoContext, + Value, + Type, + DeviceInfo, + >, + + #[device(0x01, alias = "processor")] + pub processor: host_vm::Processor, + + #[device(0x02, alias = "waveform")] + pub waveform: WaveformDevice, + + #[device(0x03, alias = "logic")] + pub logic: LogicDevice, + + #[device(0x04, alias = "sensor")] + pub sensor: SensorDevice, + + #[device(0x05, alias = "optical")] + pub optical: OpticalDevice, + + pub clock: Clock, + pub stdout: StdoutObserver, + pub optical_devices: OpticalDevices, + pub oscilloscope: Oscilloscope, + } + + instructions { + #[delegate(host_vm::Instruction)] + Processor(host_vm::Instruction) => processor { + message with resolve_processor; + effects { + observe stdout; + handle with handle_processor; + } + } + + #[delegate(WaveformInstruction)] + Waveform(WaveformInstruction) => waveform { + message with resolve_waveform; + effects { + observe optical_devices, oscilloscope; + handle with handle_waveform; + } + } + + #[delegate(LogicInstruction)] + Logic(LogicInstruction) => logic { + message with resolve_logic; + effects { + handle with handle_logic; + } + } + + #[pattern = "'get_measurement"] + Sample(SensorDevice::instruction::Sample) => sensor { + message with resolve_sample; + effects { + handle with handle_sample; + } + } + + #[pattern = "'pair_pulse"] + PairPulse(OpticalDevice::instruction::PairPulse) => optical { + message with resolve_pair_pulse; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'global_phase"] + GlobalPhase(OpticalDevice::instruction::GlobalPhase) => optical { + message with resolve_global_phase; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'global_rotation"] + GlobalRotation(OpticalDevice::instruction::GlobalRotation) => optical { + message with resolve_global_rotation; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'local_phase"] + LocalPhase(OpticalDevice::instruction::LocalPhase) => optical { + message with resolve_local_phase; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'local_rotation"] + LocalRotation(OpticalDevice::instruction::LocalRotation) => optical { + message with resolve_local_rotation; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'configure_sites"] + ConfigureSites(OpticalDevice::instruction::ConfigureSites) => optical { + message with resolve_configure_sites; + effects { + handle with handle_optical; + } + } + + #[pattern = "'read_sites"] + ReadSites(OpticalDevice::instruction::ReadSites) => optical { + message with resolve_read_sites; + effects { + observe stdout; + handle with handle_optical; + } + } + + #[pattern = "'clear"] + Clear(OpticalDevice::instruction::Clear) => optical { + message with resolve_clear; + effects { + observe stdout; + handle with handle_optical; + } + } + } +} +``` + +The generated composite surface instruction enum is the parser product. The +generated runtime instruction enum is the execution product. For example: + +```text +optical::pair_pulse + -> ControlSurfaceInstruction::PairPulse + -> RuntimeInstruction::PairPulse(OpticalDevice::instruction::PairPulse) + -> resolve_pair_pulse + -> Execute +``` + +The route declaration owns the machine-specific association between surface +syntax, runtime product, component field, message resolution, observation, and +effect handling. Component declarations remain reusable: they provide typed +runtime products and `Execute` implementations, while the composite chooses +which products become part of its SST vocabulary. From cefb5cfe5af922fbcdfc02a512686c6ad03a756e Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Thu, 6 Aug 2026 15:05:46 -0400 Subject: [PATCH 05/15] Updated docs to the in-progress rewrite --- .gitignore | 3 + README.md | 60 +- demos/examples/demo-vihaco-concepts.md | 714 +--- docs/examples/counter.rs | 66 +- docs/examples/quickstart.rs | 86 +- docs/pnpm-lock.yaml | 3328 +++++++++++++++++ docs/pnpm-workspace.yaml | 2 + docs/src/pages/guide/components.md | 284 +- docs/src/pages/guide/composites.md | 427 +-- docs/src/pages/guide/index.md | 10 +- docs/src/pages/guide/instructions-advanced.md | 6 +- docs/src/pages/guide/instructions.md | 19 +- docs/src/pages/guide/messages.md | 379 +- docs/src/pages/guide/observers.md | 500 +-- docs/src/pages/index.astro | 4 +- docs/src/pages/quickstart.astro | 29 +- package-lock.json | 30 + package.json | 5 + vision/composite-syntax-runtime-plan.md | 566 +++ 19 files changed, 4369 insertions(+), 2149 deletions(-) create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/pnpm-workspace.yaml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 vision/composite-syntax-runtime-plan.md diff --git a/.gitignore b/.gitignore index 10f3ee21..f5e5cd6d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ target CLAUDE.md .agents/skills/agents-update .claude/skills/agents-update + +# docs site +node_modules \ No newline at end of file diff --git a/README.md b/README.md index b7f80cf1..2a2a0fcb 100644 --- a/README.md +++ b/README.md @@ -12,48 +12,30 @@ machine. vihaco is a framework for building small virtual machines. You define -- the **instruction set** — an enum, with `#[derive(Instruction)]`; -- the **components** that execute it — with `#[component]`; -- the **effects** they emit; and -- (optionally) **SST source syntax** — with `#[derive(Parse)]`, +- reusable **components** and their instruction products with `component!`; +- one `Execute` implementation per product, with typed messages and effects; +- **composite routes** with `composite!`; and +- (optionally) **SST source syntax** with the parser derives, -all as ordinary Rust, then compose them into a machine. A component is one -`execute(instruction, message) -> effects`: +all as ordinary Rust. A component step is +`execute(&instruction, message) -> StepResult`: ```rust use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; +use vihaco::{component, Effects, Execute, Execution, StepResult}; -// Bytecode-visible operations: each variant is an opcode, tuple fields its payload. -#[derive(Debug, Clone, Instruction)] -pub enum CounterInst { - Add(i64), - Print, +component! { + component Counter { value: i64, } + instruction { Add(i64), Read, } } -// Runtime-supplied input, not encoded in the instruction stream. -#[derive(Debug, Clone, Message)] -pub struct Prefix(pub String); - -// A value the component emits for the runtime / observers to consume. -#[derive(Debug, Clone, PartialEq)] -pub struct Line(pub String); - -#[derive(Debug, Default)] -pub struct Counter { - value: i64, -} - -#[component(instruction = CounterInst, message = Prefix, effect = Line)] -impl Counter { - fn execute(&mut self, inst: CounterInst, msg: Prefix) -> Result> { - match inst { - CounterInst::Add(v) => { - self.value += v; - Ok(Effects::none()) - } - CounterInst::Print => Ok(Effects::one(Line(format!("{}{}", msg.0, self.value)))), - } +impl Execute for counter::Counter { + type Message = (); + type Effect = (); + type Fault = eyre::Report; + fn execute(&mut self, instruction: &counter::instruction::Add, _: ()) -> Result> { + self.value += instruction.0; + Ok(StepResult { effects: Effects::none(), execution: Execution::Complete }) } } ``` @@ -65,16 +47,16 @@ needs; there is no umbrella crate. | Crate | Role | |---|---| -| [`vihaco`](crates/vihaco) | The batteries-included facade: re-exports every crate below at stable paths (`Instruction` / `Message` / `Effects`, the `#[component]` / `#[observe]` / `#[composite]` macros, the module / syntax / runtime layers, the `Value` / `Type` model), so most projects depend only on this crate. | +| [`vihaco`](crates/vihaco) | The batteries-included facade: re-exports the instruction, message, effects, execution, component, and composite APIs, plus the module / syntax / runtime layers and `Value` / `Type` model. | | [`vihaco-abi`](crates/vihaco-abi) | The ISA vocabulary: the `Instruction` / `Effects` types, the `Value` / `Type` model, and the encoding + host-VM traits. | | [`vihaco-abi-derive`](crates/vihaco-abi-derive) | `#[derive(Instruction)]`, re-exported through `vihaco-abi`'s `derive` feature. | | [`vihaco-bytecode`](crates/vihaco-bytecode) | The binary / SST container format: headers, sections, and instruction (de)coding. | | [`vihaco-module`](crates/vihaco-module) | The loadable `Module` model, program loader, host-VM traits, and assembly-style `Display`. | -| [`vihaco-runtime`](crates/vihaco-runtime) | The component/machine runtime: `GeneratedComponent`, effect sinks, and observation machinery. | -| [`vihaco-runtime-derive`](crates/vihaco-runtime-derive) | `#[derive(Message)]`, `#[component]`, `#[composite]`, `#[observe]`, re-exported through `vihaco-runtime`'s `derive` feature. | +| [`vihaco-runtime`](crates/vihaco-runtime) | The component/machine runtime: `Execute`, `StepResult`, `Execution`, `Supply`, `Absorb`, `Observe`, `Handle`, and effect machinery. | +| [`vihaco-runtime-derive`](crates/vihaco-runtime-derive) | The `component!` and `composite!` declaration macros, re-exported through `vihaco` and `vihaco-runtime`'s `derive` feature. | | [`vihaco-stdlib`](crates/vihaco-stdlib) | Standard-library components and observers, including `StdoutObserver`. | | [`vihaco-syntax`](crates/vihaco-syntax) | Typed SST parsing and module construction (`Resolve`). | -| [`vihaco-cpu`](crates/vihaco-cpu) | A ready-made CPU/host component — a small stack machine (constants, arithmetic, branches, halt, …) with a `StepOutcome` control-flow effect. Use directly, or as a reference for writing your own. | +| [`vihaco-cpu`](crates/vihaco-cpu) | A ready-made CPU/host component — a small stack machine (constants, arithmetic, branches, halt, …). Use directly, or as a reference for writing your own. | | [`vihaco-parser`](crates/vihaco-parser) | The `Parse<'src>` and `SurfaceInstruction` traits plus lexical, primitive, and collection implementations shared by the parser derive. | | [`vihaco-parser-derive`](crates/vihaco-parser-derive) | `#[derive(Parse)]` — turns instruction, value, and type enums or structs into [chumsky](https://github.com/zesterer/chumsky) parsers via `#[syntax_class]` and `#[pattern]`. | diff --git a/demos/examples/demo-vihaco-concepts.md b/demos/examples/demo-vihaco-concepts.md index 2b5befff..6700315d 100644 --- a/demos/examples/demo-vihaco-concepts.md +++ b/demos/examples/demo-vihaco-concepts.md @@ -1,324 +1,24 @@ -# Concepts in the Demo's `vihaco` Layer +# vihaco concepts used by the demo -The files under [`demo/vihaco`](./demo/vihaco) contain the small -contracts used to express an instruction pipeline. They are not a complete framework API. They are -a concrete sketch of the relationships the eventual framework and its macros need to generate. +The demo separates reusable components from the machine-specific runtime that +contains them. Its small contracts mirror the current `vihaco` runtime API; +the larger event loop remains ordinary Rust so the ownership boundaries are +visible. -This document explains each concept independently. The examples use deliberately small domains -such as a counter, a mailbox, and a door; they are not taken from the demo machine. +## Components and products -## Concept status against current vihaco - -The contracts described here are a design sketch, not a claim that every concept is already part -of vihaco core. The following map shows how they relate to the current implementation: - -| Concept document | Current vihaco equivalent | Comparison | -|---|---|---| -| `Effects` | [`effect.rs`](../../crates/vihaco/src/effect.rs) | Already exists closely. Current `Effects` supports `None`, `One`, `Many`, mapping, flattening, and iteration. | -| `NoMessage` | [`runtime/marker.rs`](../../crates/vihaco/src/runtime/marker.rs) | The demo uses a named `NoMessage` type. Current vihaco has a general `Message` marker trait and implements it for `()`, but does not provide the same named convention. | -| `Execution` | [`vihaco-cpu/src/outcome.rs`](../../crates/vihaco-cpu/src/outcome.rs) | Current `StepOutcome` models CPU outcomes such as `Continue`, `Breakpoint`, `Halt`, and `Return`. It is broader and different from the demo's `Complete`/`Parked` suspension state. | -| `StepResult` | [`runtime/generated.rs`](../../crates/vihaco/src/runtime/generated.rs) | The demo groups effects and execution state in `StepResult`. Current generated components return `Result>`; execution state is not paired with effects in one core type. | -| `Execute` | [`#[component]`](../../crates/vihaco-derive/src/attr_component.rs) and [`GeneratedComponent`](../../crates/vihaco/src/runtime/generated.rs) | The demo uses one `Execute` implementation per instruction type. Current vihaco uses one component-level `execute` method over an instruction type, message type, and effect type, then generates `GeneratedComponent`. | -| `Supply` | [`StackMemory`](../../crates/vihaco/src/traits/machine.rs) and component-specific APIs | The demo has a general typed message-supply capability. Current vihaco has specialized state-access traits such as `StackMemory`, but no general `Supply` trait. | -| `Absorb` | [`EffectSink`](../../crates/vihaco/src/traits/event_sink.rs) | Both describe effect destinations, but `EffectSink` emits into a sink and has no fault result. The demo's `Absorb` models a component actively consuming and applying an effect. | -| `Observe` | [`runtime::Observe`](../../crates/vihaco/src/runtime/observe.rs) | Current observation can return follow-up effects, but it has no `Route` type parameter. | -| `Handle` | No direct equivalent | Current vihaco has `EffectSink` and generated dispatch, but not a route-parameterized handler with a default `Absorb` delegation path. | -| `Route` | Generated composite/device metadata | Current [`#[composite]`](../../crates/vihaco-derive/src/attr_composite.rs) and `Machine` machinery generate device and instruction routing, but the explicit per-route marker trait in this document is not currently a public core concept. | -| `Resume` | No direct core equivalent | The demo models owned completion of a parked operation explicitly. Current runtime traits do not yet expose the same generic resume contract. | -| `component!` instruction expansion | [`#[component]`](../../crates/vihaco-derive/src/attr_component.rs) | These are different layers. Current `#[component]` adapts an implementation over one instruction enum into `GeneratedComponent`; it does not split an enum into individual instruction structs. | -| `machine!` effect fanout | [`#[composite]`](../../crates/vihaco-derive/src/attr_composite.rs), [`#[observe]`](../../crates/vihaco-derive/src/attr_observe.rs), and generated dispatch | Current macros generate machine/device structure and observation support, but the planned `effects { observe ...; to ...; with ...; }` syntax does not currently exist. | - -The status of these relationships can be summarized as: - -- **Current** — the repository already provides approximately the same concept. -- **Partial** — the repository provides a related mechanism with different ownership or type - boundaries. -- **Proposed** — the concept is demonstrated here but is not currently part of vihaco core. -- **Planned macro surface** — the concept describes intended syntax or code generation that is not - implemented yet. - -### The important `Execute` difference - -The conceptual design has individually executable instruction products: - -```rust -impl Execute for ArithmeticUnit { - type Message = BinaryOperands; - type Effect = ValueResult; - type Fault = ArithmeticFault; - - fn execute( - &mut self, - instruction: &Add, - message: BinaryOperands, - ) -> Result, ArithmeticFault> { - // execute one instruction product - } -} -``` - -Current vihaco instead uses a component-level instruction sum: - -```rust -#[component( - instruction = RuntimeInstruction, - message = CPUMessage, - effect = StepOutcome, -)] -impl CPU { - fn execute( - &mut self, - instruction: RuntimeInstruction, - message: CPUMessage, - ) -> eyre::Result> { - match (instruction, message) { - // current component-level dispatch - } - } -} -``` - -The current `#[component]` macro generates an implementation of `GeneratedComponent`: - -```rust -trait GeneratedComponent { - type Instruction; - type Message; - type Effect; - - fn execute_generated( - &mut self, - instruction: Self::Instruction, - message: Self::Message, - ) -> eyre::Result>; -} -``` - -The conceptual direction is therefore more granular than the current implementation. It aims to -move instruction matching and each instruction's message/effect relationship into separate -`Execute` implementations. - -## The pipeline at a glance - -An instruction usually crosses four boundaries: - -```text -component state --Supply--> message --Execute--> effects + execution state - | - Observe (borrow) --+ - Handle (consume) --+ -``` - -If execution cannot finish immediately, the component returns `Parked`. Later, an owned completion -is given to `Resume`, which produces another `StepResult`. The parent composite owns the sequencing -and decides what to do with the result; the instruction component owns its local invariants. - -The `Effects` type in the examples comes from the surrounding framework. It represents zero, -one, or many effects. The contracts in this directory specify how those effects are produced and -consumed, but do not define the collection itself. - -## `NoMessage`: making “no input” explicit - -`NoMessage` is a marker type for an instruction whose execution needs no value resolved from the -runtime. It is preferable to using `()` everywhere because it gives the route a named, searchable -contract and leaves room for framework-level policies around message resolution. - -For example, a `ResetDisplay` instruction can state that it has no runtime input: - -```rust -struct ResetDisplay; - -impl Execute for Display { - type Message = NoMessage; - type Effect = DisplayReset; - type Fault = DisplayFault; - - fn execute( - &mut self, - _instruction: &ResetDisplay, - _message: NoMessage, - ) -> Result, DisplayFault> { - self.clear_pixels(); - Ok(StepResult { - effects: Effects::one(DisplayReset), - execution: Execution::Complete, - }) - } -} -``` - -The important distinction is between “no message is needed” and “the message happens to be an -empty value.” A route requiring a `UserId` cannot accidentally be wired to `NoMessage`, and a -component that supplies messages can be checked against the exact instruction type. - -## `Execution`: whether the instruction finished - -`Execution` has two states: - -```rust -enum Execution { - Complete, - Parked, -} -``` - -`Complete` means the parent may advance the instruction stream. `Parked` means the current -instruction is still the active instruction and must be resumed or otherwise resolved before the -parent advances. - -This state is separate from effects. An instruction can emit an effect and still park. For -example, a `WaitForDoor` operation may emit a `WaitRegistered` fact while it waits for an external -signal: - -```text -effects: [WaitRegistered] -execution: Parked -``` - -Keeping these dimensions separate prevents a parent from inferring completion merely because an -effect was emitted. It also means an effect handler can schedule a wakeup without having to mutate -the child program counter. - -## `StepResult`: the result of starting or resuming work - -`StepResult` groups the effects produced by one execution attempt with its completion state: +`component!` declares a component and its owned runtime instruction products: ```rust -struct StepResult { - effects: Effects, - execution: Execution, -} -``` - -The same shape is returned by `Execute` and `Resume`. That is useful because the parent can run -the same observation and handling pipeline after an instruction starts and after a parked -instruction wakes. - -Consider a queue read. A successful read might return: - -```text -StepResult { - effects: [ItemRead(42)], - execution: Complete, -} -``` - -An empty queue might return: - -```text -StepResult { - effects: [ReaderParked(reader_id)], - execution: Parked, -} -``` - -The parent does not need separate “normal result” and “suspension result” plumbing. It still -processes effects, then branches on `execution`. - -## `Execute`: component-owned instruction behavior - -`Execute` says that a component can execute one particular instruction type: - -```rust -trait Execute { - type Message; - type Effect; - type Fault; - - fn execute( - &mut self, - instruction: &I, - message: Self::Message, - ) -> Result, Self::Fault>; -} -``` - -The instruction, message, effect, and fault are associated with this specific implementation. -That is more precise than giving a component one large enum and one universal message type. - -For a simple `AddCredit` operation: - -```rust -struct AddCredit; -struct CreditAmount(u64); -struct CreditChanged(u64); - -impl Execute for Wallet { - type Message = CreditAmount; - type Effect = CreditChanged; - type Fault = WalletFault; - - fn execute( - &mut self, - _instruction: &AddCredit, - CreditAmount(amount): CreditAmount, - ) -> Result, WalletFault> { - self.balance = self - .balance - .checked_add(amount) - .ok_or(WalletFault::Overflow)?; - Ok(StepResult { - effects: Effects::one(CreditChanged(self.balance)), - execution: Execution::Complete, - }) - } -} -``` - -This allows the same `AddCredit` instruction to be selected into multiple composites, provided -each composite supplies a compatible message and handles the declared effect. The `Wallet` owns -the balance invariant; the composite owns how the message is obtained and where the effect goes. - -## `component!`: declaring a component's instruction set - -The arithmetic library shows the shape that a future `component!` macro is meant to make concise. -`ArithmeticUnit` is a reusable component, and its instruction set consists of `add`, `sub`, and -`mul`. The source currently writes the important pieces out by hand. Its commented `isa!` sketch -shows the intended declaration: - -```rust -isa! { - #[namespace("arith")] - instruction Arithmetic { - #[pattern = "'add"] - Add, - #[pattern = "'sub"] - Sub, - #[pattern = "'mul"] - Mul, - } -} -``` - -A component-oriented macro can use that instruction set as part of a declaration such as: - -```text component! { - ArithmeticUnit { - instructions: Arithmetic, - } -} -``` - -The macro's useful expansion is not one `Execute` implementation. It turns each -instruction-set member into an individual instruction struct and preserves an enum of those -structs for grouping, parsing, storage, or dispatch: - -```rust -struct Add; -struct Sub; -struct Mul; - -enum Arithmetic { - Add(Add), - Sub(Sub), - Mul(Mul), + component Stack { items: Vec, } + instruction { Push(i64), Pop, } } ``` -The enum is the instruction *sum*: it answers “which arithmetic operation is this value?” The -structs are the individual instruction *products*: each one can be used as the `I` in -`Execute`: +The implementation is per product. The demo's arithmetic unit implements +`Execute`, `Execute`, and `Execute` independently. Each +implementation chooses its own `Message`, `Effect`, and `Fault` types. ```rust impl Execute for ArithmeticUnit { @@ -328,384 +28,54 @@ impl Execute for ArithmeticUnit { fn execute( &mut self, - _instruction: &Add, - message: BinaryOperands, + _: &Add, + operands: BinaryOperands, ) -> Result, ArithmeticFault> { Ok(StepResult { - effects: Effects::one(ValueResult(message.lhs + message.rhs)), + effects: Effects::one(ValueResult(operands.lhs + operands.rhs)), execution: Execution::Complete, }) } } ``` -`Sub` and `Mul` can have their own `Execute` and `Execute` implementations. They may -share `BinaryOperands` and `ValueResult`, as the arithmetic component does, or declare different -message, effect, and fault types when their semantics require it. - -This split is necessary because a single enum implementation would force execution through a -large match and one broad set of associated types. Individual structs allow the type system to -record that `Add` needs `BinaryOperands`, produces `ValueResult`, and has a particular fault -model. A composite can select only `Add` without also exposing `Sub` and `Mul`, while a parser or -runtime instruction sum can still carry all three variants when it needs a single storable value. - -The component macro therefore has two related jobs: - -1. Declare or consume the component's instruction set and generate the individual instruction - products plus their grouped enum. -2. Generate the repetitive component boundary and dispatch plumbing while leaving the actual - `Execute` behavior to the component author. - -The component owns reusable instruction behavior. A composite later decides which individual -instructions are admitted, which component instance receives each one, where messages come from, -and where effects go. This keeps instruction semantics reusable without making every component -automatically expose every operation in every machine. - -## `Supply`: resolving a runtime message - -`Supply` is a capability for obtaining a message of type `M` from component state: - -```rust -trait Supply { - type Fault; - - fn supply(&mut self) -> Result; -} -``` - -For the wallet example, a route might supply an amount from a register component: - -```rust -struct Register(u64); - -impl Supply for Register { - type Fault = RegisterFault; - - fn supply(&mut self) -> Result { - Ok(CreditAmount(self.0)) - } -} -``` - -The capability keeps message resolution outside `Execute`. `Wallet` does not need to know -whether its amount came from a register, a decoded constant, a stack, or a network adapter. A -different machine can reuse `AddCredit` with a different `Supply` implementation. - -For `NoMessage`, no supplier is needed: the framework can construct `NoMessage` directly. - -## `Absorb`: a reusable effect destination - -`Absorb` describes a component that can consume an effect: - -```rust -trait Absorb { - type Fault; - - fn absorb(&mut self, effect: E) -> Result<(), Self::Fault>; -} -``` - -A history component can absorb wallet changes without knowing how the wallet produced them: - -```rust -struct AuditLog(Vec); - -impl Absorb for AuditLog { - type Fault = std::convert::Infallible; - - fn absorb(&mut self, CreditChanged(balance): CreditChanged) -> Result<(), Self::Fault> { - self.0.push(format!("balance is now {balance}")); - Ok(()) - } -} -``` - -This enables reuse and composition. The same `CreditChanged` can be handled by a balance display, -an audit log, or a quota checker, each with its own state and fault type. `Absorb` is intentionally -machine-agnostic: it says what a component can consume, not which instruction route selected it. - -The component author implements `Absorb` as part of the component's reusable behavior. The -composite author, or generated composite code, supplies the route-specific `Handle` wiring -that decides when and where the capability is used. This is why `Absorb` does not need to know the -route that produced the effect. - -## `Observe`: non-consuming instrumentation - -`Observe` receives a borrowed effect before the semantic handler consumes it: - -```rust -trait Observe { - type Error; - - fn observe(&mut self, effect: &Effect) -> Result<(), Self::Error>; -} -``` - -The route parameter matters because the same effect type may be produced by several routes. A -simple observer can count events without taking ownership: - -```rust -struct CreditRoute; -struct Metrics { credit_events: usize } - -impl Observe for Metrics { - type Error = std::convert::Infallible; - - fn observe(&mut self, _effect: &CreditChanged) -> Result<(), Self::Error> { - self.credit_events += 1; - Ok(()) - } -} -``` - -Observation is separate from handling for two reasons. First, logging and metrics should not -become the semantic owner of an effect. Second, multiple observers can borrow the same effect in a -deterministic order before one handler consumes it. Enabling an observer should add visibility, -not change the destination or ownership of the effect. - -## `Handle`: route-specific effect handling - -`Handle` consumes an effect for one statically identified route: - -```rust -trait Handle { - type Error; - - fn handle(&mut self, effect: Effect) -> Result<(), Self::Error>; -} -``` - -The route parameter prevents ambiguous handling when one composite selects the same effect or -instruction more than once. For example, a machine could route `MessageSent` from two different -ports to one transport type while keeping their destinations distinct: - -```rust -struct LeftPort; -struct RightPort; -struct Transport; -struct MessageSent(Vec); - -impl Handle for Transport { - type Error = TransportFault; - - fn handle(&mut self, effect: MessageSent) -> Result<(), TransportFault> { - self.send_from_left(effect.0) - } -} - -impl Handle for Transport { - type Error = TransportFault; - - fn handle(&mut self, effect: MessageSent) -> Result<(), TransportFault> { - self.send_from_right(effect.0) - } -} -``` - -Without the route marker, the two implementations would collide because Rust sees the same -`Transport` target and `MessageSent` effect. More importantly, the generated composite would lose -the identity needed to route each operation correctly. +## Composite routes -In the usual case, `Handle` is the route-aware adapter and `Absorb` is the reusable destination -capability. The generated or hand-written `Handle` implementation commonly delegates directly to -`Absorb`: +The demo's CPU uses `composite!` to select the products it exposes and connect +them to capabilities: ```rust -impl Handle for AuditLog { - type Error = >::Fault; - - fn handle(&mut self, effect: CreditChanged) -> Result<(), Self::Error> { - self.absorb(effect) +composite! { + composite Cpu { + error = CpuFault; + operand_stack: Stack, + alu: ArithmeticUnit, } -} -``` - -This preserves both roles: `Absorb` says that `AuditLog` can consume this effect in -any suitable context, while `Handle` says that this particular machine -route sends its effect to that destination. `Handle` can instead contain route-specific behavior -when the default delegation is not sufficient. - -## `Route`: static identity for one selected path - -`Route` is a marker trait with associated `Effect` and `Error` types: - -```rust -trait Route { - type Effect; - type Error; -} -``` - -A route is not a runtime event and not a program-counter state. It is the compile-time identity of -one path through a composite. A generated composite might create markers like these: - -```rust -struct ReadConfig; -struct ReadSecret; - -impl Route for ReadConfig { - type Effect = ConfigRead; - type Error = MachineFault; -} - -impl Route for ReadSecret { - type Effect = SecretRead; - type Error = MachineFault; -} -``` - -Route identity allows generation to associate each path with its own message supplier, component, -effect observers, handler, timing policy, and diagnostics. It also makes it possible to route the -same instruction type to two component instances without merging their wiring. - -The associated `Effect` lets generated code name the route once and derive the effect type from -it. The associated `Error` is the containing machine's normalized error boundary: lower-level -component, supplier, observer, and handler faults can be converted into it at the route boundary. - -## `Resume`: completing a parked operation - -`Resume` handles a completion for an operation that previously returned `Parked`: - -```rust -trait Resume { - type Effect; - type Fault; - - fn resume(&mut self, completion: C) -> Result, Self::Fault>; -} -``` - -The completion must be owned. It cannot contain a borrow into the parent or into a temporary -message because the parent may process it much later. - -For a door controller: - -```rust -struct OpenDoor; -struct DoorOpened; -struct OpenCompletion { request_id: u64 }; -impl Resume for DoorController { - type Effect = DoorOpened; - type Fault = DoorFault; - - fn resume( - &mut self, - completion: OpenCompletion, - ) -> Result, DoorFault> { - self.finish_request(completion.request_id)?; - Ok(StepResult { - effects: Effects::one(DoorOpened), - execution: Execution::Complete, - }) + runtime_instructions { + IntegerAdd(Add) => alu { + message from operand_stack; + effects { absorb with operand_stack; } + } } } ``` -The parent stores or schedules `OpenCompletion`; it does not need to understand the controller's -internal state. When resumed, the controller can emit ordinary effects and use the same handling -pipeline as a newly started instruction. - -## `machine_macro.rs`: planned effect fanout - -This file is currently a design note, not an implementation. It sketches a future `machine!` -surface for declaring effect fanout: - -```text -effects { - observe metrics, trace; - to audit_log; -} -``` - -The intended expansion is: - -```text -for each effect: - metrics.observe(&effect) - trace.observe(&effect) - audit_log.handle(effect) -``` - -The observers borrow the effect, so both can inspect it. The handler receives ownership exactly -once. `to audit_log;` selects the default behavior: the generated `Handle` implementation routes -the effect to `audit_log`, normally by calling its `Absorb` implementation. - -When the machine needs custom effect-handling behavior, the destination can eventually be -overridden with `with record_credit;`: - -```text -effects { - observe metrics, trace; - with record_credit; -} -``` - -`with record_credit;` names a handler function supplied by the machine author. The generated route -uses that function instead of the default `Handle`/`Absorb` path. For example, the machine author -could provide: - -```rust -fn record_credit( - machine: &mut AccountMachine, - effect: CreditChanged, -) -> Result<(), AccountFault> { - machine.audit.push(effect.0); - Ok(()) -} -``` - -This syntax is necessary because effect routing is repetitive but semantically important: -the generated code must preserve observer order, handler ownership, route identity, and error -conversion. - -The macro should generate wiring, not invent behavior. The author still defines the component's -`Execute` implementation, the observer logic, and the handler logic. The declaration merely makes -the selected connections visible and checks that the types fit. - -## How the concepts fit together - -Here is a complete small route for `AddCredit`: - -```text -Register::supply - -> CreditAmount - -> Wallet::execute(AddCredit, CreditAmount) - -> StepResult - -> Metrics::observe(&CreditChanged) - -> AuditLog::handle(CreditChanged) - -> Execution::Complete -``` - -The same route with a waiting instruction has a different control state but the same effect -pipeline: +The generated `CpuInstruction` is a machine-local sum. A route resolves an +owned message, executes the selected product, observes each effect, and sends +ownership to one handler. `Supply` and `Absorb` keep the reusable stack +independent of this particular CPU. -```text -Mailbox::execute(ReadNext, NoMessage) - -> StepResult - -> Execution::Parked - -> later ReadCompletion - -> Mailbox::resume(ReadCompletion) - -> StepResult - -> observers and handler - -> Execution::Complete -``` +## Effects and parked work -Together, these contracts provide the useful separation: +`Effects` represents zero, one, or many homogeneous effects. `StepResult` +pairs those effects with `Execution::Complete` or `Execution::Parked`. -- `Supply` determines where runtime input comes from. -- `Execute` owns the instruction's local state transition. -- `Effects` communicates consequences without exposing component internals. -- `Observe` adds non-owning diagnostics and instrumentation. -- `Absorb` provides the reusable effect-consuming capability, while `Handle` normally delegates to - it and adds route identity; a `machine!` `with handler;` clause can eventually override that - default. -- `Execution` tells the parent whether instruction-stream progress is allowed. -- `Resume` gives suspension a typed, owned completion path. -- `Route` keeps repeated or identical-looking paths distinct. -- `NoMessage` makes the absence of runtime input explicit. -- The planned macro makes the wiring concise while preserving those boundaries. +The demo's receive operation can park. The child owns its continuation and +knows how to resume it; the parent owns the event loop, global clock, endpoint +identity, and scheduling policy. This keeps a reusable channel from knowing +whether it is hosted by one CPU or several. -That separation is what lets a single instruction behavior be reused in different machines, lets a -component retain ownership of its invariants, and lets a parent composite coordinate dataflow and -suspension without reaching into child-private state. +Timing and continuation dispatch are intentionally hand-written in the current +API. A future extension may generate more of that plumbing; until it exists, +the demo's explicit root loop is the authoritative pattern. diff --git a/docs/examples/counter.rs b/docs/examples/counter.rs index 265c344d..cbdc4385 100644 --- a/docs/examples/counter.rs +++ b/docs/examples/counter.rs @@ -1,55 +1,37 @@ use eyre::Result; -use vihaco::{Effects, Execute, Execution, Instruction, Message, StepResult}; +use vihaco::{component, Effects, Execute, Execution, StepResult}; -/// Bytecode-visible operations. Each variant becomes an opcode; tuple -/// fields become the payload bytes that follow it. -#[derive(Debug, Clone, Instruction)] -pub enum CounterInst { - Add(i64), - Print, -} - -/// Resolved execution input — supplied by the runtime, not encoded in -/// the instruction stream. -#[derive(Debug, Clone)] -pub struct Prefix(pub String); +component! { + component Counter { + value: i64, + } -impl Message for Prefix {} + instruction { + Add(i64), + Read, + } +} -/// A value the component returns for the runtime or observers to consume. -#[derive(Debug, Clone, PartialEq)] -pub struct Line(pub String); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Value(pub i64); -#[derive(Debug, Default)] -pub struct Counter { - value: i64, -} +impl Execute for counter::Counter { + type Message = (); + type Effect = (); + type Fault = eyre::Report; -impl Counter { - fn execute_instruction(&mut self, inst: &CounterInst, msg: Prefix) -> Result> { - match inst { - CounterInst::Add(v) => { - self.value += v; - Ok(Effects::none()) - } - CounterInst::Print => Ok(Effects::one(Line(format!("{}{}", msg.0, self.value)))), - } + fn execute(&mut self, instruction: &counter::instruction::Add, _: ()) -> Result> { + self.value += instruction.0; + Ok(StepResult { effects: Effects::none(), execution: Execution::Complete }) } } -impl Execute for Counter { - type Message = Prefix; - type Effect = Line; +impl Execute for counter::Counter { + type Message = (); + type Effect = Value; type Fault = eyre::Report; - fn execute( - &mut self, - inst: &CounterInst, - msg: Prefix, - ) -> Result> { - Ok(StepResult { - effects: self.execute_instruction(inst, msg)?, - execution: Execution::Complete, - }) + fn execute(&mut self, _: &counter::instruction::Read, _: ()) -> Result> { + Ok(StepResult { effects: Effects::one(Value(self.value)), execution: Execution::Complete }) } } diff --git a/docs/examples/quickstart.rs b/docs/examples/quickstart.rs index 6861659e..b001fc56 100644 --- a/docs/examples/quickstart.rs +++ b/docs/examples/quickstart.rs @@ -1,71 +1,49 @@ use eyre::Result; -use vihaco::{ - Effects, Execute, Execution, Instruction, Message, StepResult, expect_exactly_one_effect, -}; +use vihaco::{component, Effects, Execute, Execution, StepResult}; -#[derive(Debug, Clone, Instruction)] -pub enum CounterInst { - Add(i64), - Print, -} - -#[derive(Debug, Clone)] -pub struct Prefix(pub String); +component! { + component Counter { + value: i64, + } -impl Message for Prefix {} + instruction { + Add(i64), + Read, + } +} -#[derive(Debug, Clone, PartialEq)] -pub struct Line(pub String); +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Value(pub i64); -#[derive(Debug, Default)] -pub struct Counter { - value: i64, -} +impl Execute for counter::Counter { + type Message = (); + type Effect = (); + type Fault = eyre::Report; -impl Counter { - fn execute_instruction(&mut self, inst: &CounterInst, msg: Prefix) -> Result> { - match inst { - CounterInst::Add(v) => { - self.value += v; - Ok(Effects::none()) - } - CounterInst::Print => Ok(Effects::one(Line(format!("{}{}", msg.0, self.value)))), - } + fn execute(&mut self, instruction: &counter::instruction::Add, _: ()) -> Result> { + self.value += instruction.0; + Ok(StepResult { effects: Effects::none(), execution: Execution::Complete }) } } -impl Execute for Counter { - type Message = Prefix; - type Effect = Line; +impl Execute for counter::Counter { + type Message = (); + type Effect = Value; type Fault = eyre::Report; - fn execute( - &mut self, - inst: &CounterInst, - msg: Prefix, - ) -> Result> { - Ok(StepResult { - effects: self.execute_instruction(inst, msg)?, - execution: Execution::Complete, - }) + fn execute(&mut self, _: &counter::instruction::Read, _: ()) -> Result> { + Ok(StepResult { effects: Effects::one(Value(self.value)), execution: Execution::Complete }) } } fn main() -> Result<()> { - let mut counter = Counter::default(); - - // `Add` ignores its message and returns no effects. - Execute::execute(&mut counter, &CounterInst::Add(2), Prefix(String::new()))?; - Execute::execute(&mut counter, &CounterInst::Add(3), Prefix(String::new()))?; - - // `Print` returns exactly one `Line` effect. - let effects = Execute::execute( - &mut counter, - &CounterInst::Print, - Prefix("total = ".into()), - )? - .effects; - let line = expect_exactly_one_effect(effects)?; - assert_eq!(line, Line("total = 5".into())); + let mut counter = counter::Counter { value: 0 }; + Execute::execute(&mut counter, &counter::instruction::Add(5), ())?; + let value = Execute::execute(&mut counter, &counter::instruction::Read, ())? + .effects + .into_iter() + .next() + .expect("Read emits one value"); + assert_eq!(value, Value(5)); Ok(()) } diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 00000000..b0360ca9 --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,3328 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/markdown-remark': + specifier: ^7.2.1 + version: 7.2.2 + astro: + specifier: ^7.1.1 + version: 7.1.6(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + +packages: + + '@astrojs/compiler-binding-darwin-arm64@0.3.2': + resolution: {integrity: sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@astrojs/compiler-binding-darwin-x64@0.3.2': + resolution: {integrity: sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': + resolution: {integrity: sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': + resolution: {integrity: sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': + resolution: {integrity: sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@astrojs/compiler-binding-linux-x64-musl@0.3.2': + resolution: {integrity: sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@astrojs/compiler-binding-wasm32-wasi@0.3.2': + resolution: {integrity: sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': + resolution: {integrity: sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': + resolution: {integrity: sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@astrojs/compiler-binding@0.3.2': + resolution: {integrity: sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@astrojs/compiler-rs@0.3.2': + resolution: {integrity: sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==} + engines: {node: '>=22.12.0'} + + '@astrojs/internal-helpers@0.10.2': + resolution: {integrity: sha512-yt7fMgPYqSM4Tmr+taTW6Per+hjJ8Pk6lA1PAcDyqzOt8HzJ6Kje5WzCxA2Sd+9wsUW7uhkLeoTMK0cXPwH9rQ==} + + '@astrojs/markdown-remark@7.2.2': + resolution: {integrity: sha512-FGfmK84zSNcrsBd0dl1gXE9JvZYElp8EXQa2jpHVAxG4deGKAp43wspxFupjADJX7MSsMRHwYCnfT6EyVmgeFQ==} + + '@astrojs/markdown-satteri@0.3.5': + resolution: {integrity: sha512-CvWVEFAbay7YO+i9SaqDJubipA5ckiVB89QWoMJ5XC0m5CtFg8JwZ7Kau6X9sYY7FZURH0w2l03ISH2jOS/RDQ==} + + '@astrojs/prism@4.0.2': + resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} + engines: {node: '>=22.12.0'} + + '@astrojs/telemetry@3.3.3': + resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bruits/satteri-darwin-arm64@0.9.5': + resolution: {integrity: sha512-iw4nZgx9v30lWo/MTngQqi1pI78KI0DnkSm+lVJGYdmPLgAyDNJigVhpG42/Iq55A6c1Ll8q66ljyyRiQUxwow==} + cpu: [arm64] + os: [darwin] + + '@bruits/satteri-darwin-x64@0.9.5': + resolution: {integrity: sha512-6T26Z5Kf3cFW2PSlk9p7zT7yVxvuBSiJvYyz9u8KjYwMTqZyIDOj2wDyNpxKV4+6yUVG7rddq2QwvG/8LJA2+Q==} + cpu: [x64] + os: [darwin] + + '@bruits/satteri-linux-arm64-gnu@0.9.5': + resolution: {integrity: sha512-u51id17uJwNEMK9nBlICsq6U31c+XVqQueVBkwRIzZG+gMpS8TOJctt5h5Wz33Z8xnMdTd+adtACVz0yHgGuOA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-arm64-musl@0.9.5': + resolution: {integrity: sha512-v39HxiwGC5Rqm01HksP6+5Y+xKLPlsuVFgIgpEAo+SiQ22c+mJVhS3u7Z6ePAKdhL5NJoK1xq70kLz3L13AhpQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@bruits/satteri-linux-x64-gnu@0.9.5': + resolution: {integrity: sha512-F3uO8uFp3pAP5ZGXttwvh57GS7s0lL953tnNdyI2gRyP4kOOkp6pyGojNJzCjkDvWI2Cvb9iNrKok3aqQPauAw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@bruits/satteri-linux-x64-musl@0.9.5': + resolution: {integrity: sha512-bicEqglLlz++mWyADaZoP0JY20s4vDfLjaPYgQqC+NI4zZLTOOg1T4GB8aqtc822Pqji8SQBmSrTb7CrP8i08Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@bruits/satteri-wasm32-wasi@0.9.5': + resolution: {integrity: sha512-zauAuMwfPnKPUkd4AFixRFpXdgKwP2mKgxrIIo2gJzW0/ZneF9dbHnLkojSpaBnCCp7VUL1hIi5WWZvB1CqmAQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@bruits/satteri-win32-arm64-msvc@0.9.5': + resolution: {integrity: sha512-SrfE7NEsgZjBvU3c+RR6oQRu0ToXY5uVJEbieXEF0YTctIV2zAVlbaMjWLts074QCgh3a+XHWkR/lWh2VH2LUg==} + cpu: [arm64] + os: [win32] + + '@bruits/satteri-win32-x64-msvc@0.9.5': + resolution: {integrity: sha512-5Kw9ZAtTGS8WHizyn+CJhjjfIQrw+7jcZodpmpXJjefnO15M8UexIi6JR2E5thyvsmHyhL6ZDDMUNR4bKJPd4g==} + cpu: [x64] + os: [win32] + + '@capsizecss/unpack@4.0.1': + resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} + engines: {node: '>=18'} + + '@clack/core@1.4.3': + resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} + engines: {node: '>= 20.12.0'} + + '@clack/prompts@1.7.0': + resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} + engines: {node: '>= 20.12.0'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.3': + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.3': + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.3': + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.2': + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.2': + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.2': + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.2': + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.2': + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.2': + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.2': + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.2': + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.3': + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.3': + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.3': + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.3': + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.3': + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.3': + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.3': + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.3': + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.3': + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.3': + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.3': + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.3': + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.3': + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.2.2': + resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@oxc-project/types@0.143.0': + resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} + + '@rolldown/binding-android-arm64@1.2.3': + resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.3': + resolution: {integrity: sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.3': + resolution: {integrity: sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.3': + resolution: {integrity: sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + resolution: {integrity: sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + resolution: {integrity: sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.3': + resolution: {integrity: sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + resolution: {integrity: sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + resolution: {integrity: sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.3': + resolution: {integrity: sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.3': + resolution: {integrity: sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.3': + resolution: {integrity: sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + resolution: {integrity: sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.3': + resolution: {integrity: sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@shikijs/core@4.4.2': + resolution: {integrity: sha512-StyzbAyxg2/tBGf78gwbBkGyeQ73lf8UiJArFaQhTQIDqQOCKPCQFanvrs4/Yv3Yfyc+ONInJM6K+FMIf+P+kA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.4.2': + resolution: {integrity: sha512-MnIkeqWdVPUWsxlx8gKLVCJFTsqrQJgpTPBPpQwaFeJ56lOnJxj5aN2LUFnfxUEcvOQuNocmbaMnVrCEln6rkw==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.4.2': + resolution: {integrity: sha512-GLhowz1+jixjz+wiZ3wMnOn1jTxiFCGl2PkXufivbnwPHKuyw1AYqu5/hbWhZZ2oAb0NP05WUJhYeigY14drnw==} + engines: {node: '>=20'} + + '@shikijs/langs@4.4.2': + resolution: {integrity: sha512-8DfeusD+Zdv/eYIDdXyJTUnSMHt+aAWjAOCXV20HNGAHRlInXpG8wh421v6B91WOm9TFwRLN+b/LG5F2NAIojg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.4.2': + resolution: {integrity: sha512-l6fQQKsOMlz72n38fztmSgZ76MO6KSWuw8o+GJ+FhmqrpC9pIOJNQNXGgbb5yX2AwpzlEHwsaLPnk/8o4Fm+rA==} + engines: {node: '>=20'} + + '@shikijs/themes@4.4.2': + resolution: {integrity: sha512-H0CFoL07ddDC2Dd6EdrPYNkRhUR6YCkJlnuYFceYYUJJA5TIm2b5B33qqiDYryBExgbKMndFJPb2u1gTuqO37g==} + engines: {node: '>=20'} + + '@shikijs/types@4.4.2': + resolution: {integrity: sha512-PFYitV4vpDr/iPCIhnHp+Q4ftic5N5VeNJ3KQ1O8gn3h2ar8qgwMAXF7tq4m1CWaMS60fV4VqF6vfnWH4F7vqQ==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + am-i-vibing@0.4.0: + resolution: {integrity: sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==} + hasBin: true + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astro@7.1.6: + resolution: {integrity: sha512-83x9rYbHazMaZkYrAFRVZXSQx2moFkz0F7cjTDUF3GWfS0a3p2vZXG1ZdhV86rStHApQCodBJW+XTD37xISIrQ==} + engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + peerDependencies: + '@astrojs/markdown-remark': 7.2.2 + peerDependenciesMeta: + '@astrojs/markdown-remark': + optional: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + common-ancestor-path@2.0.0: + resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} + engines: {node: '>= 18'} + + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + + cookie@2.0.1: + resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} + engines: {node: '>=22'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.9.0: + resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + + fast-wrap-ansi@0.2.2: + resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fontace@0.4.1: + resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} + + fontkitten@1.0.3: + resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} + engines: {node: '>=20'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@5.0.0-beta.4: + resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} + engines: {node: '>=20.20.0'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-docker@4.0.0: + resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} + engines: {node: '>=20'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + magic-string@1.1.0: + resolution: {integrity: sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g==} + + magicast@0.5.4: + resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@1.0.1: + resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + p-limit@7.3.1: + resolution: {integrity: sha512-0trZaiG7Y7kN/Egy9a8j47t9osC0Tch4PaIWd9yGF6bvmlk7muExRvGNYb8sXBwEKMoNKsbNN9P8EefuQekE4Q==} + engines: {node: '>=20'} + + p-queue@9.3.3: + resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==} + engines: {node: '>=20'} + + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + + package-manager-detector@1.8.0: + resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + piccolore@0.1.3: + resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-ancestry@0.1.0: + resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==} + engines: {node: '>=18.0.0'} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-smartypants@3.0.3: + resolution: {integrity: sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + rolldown@1.2.3: + resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + satteri@0.9.5: + resolution: {integrity: sha512-ZuWVl+vnM64y+/TtX8Kosv2c00W+hLQiiwnEL6H0UKVVrxFqMw4D2CJHHQaouVd89OAhtBBfjWLqhKi3TVUV4w==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.35.3: + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shiki@4.4.2: + resolution: {integrity: sha512-P8F/dFhRevaw2uSdeIYlq/5SXZNY85DPtmXQ947gD1Zj2JqO5AkNvVVBar0Me9JkFx3uzVud/qOtP5ek9NEQGA==} + engines: {node: '>=20'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + engines: {node: '>= 18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + svgo@4.0.2: + resolution: {integrity: sha512-ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==} + engines: {node: '>=16'} + hasBin: true + + tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + + tinyclip@0.1.15: + resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} + engines: {node: ^16.14.0 || >= 17.3.0} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + ultrahtml@1.7.0: + resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unifont@0.7.4: + resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + unstorage@1.17.5: + resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@astrojs/compiler-binding-darwin-arm64@0.3.2': + optional: true + + '@astrojs/compiler-binding-darwin-x64@0.3.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': + optional: true + + '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': + optional: true + + '@astrojs/compiler-binding-linux-x64-musl@0.3.2': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + dependencies: + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + optional: true + + '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': + optional: true + + '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': + optional: true + + '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + optionalDependencies: + '@astrojs/compiler-binding-darwin-arm64': 0.3.2 + '@astrojs/compiler-binding-darwin-x64': 0.3.2 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.2 + '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 + '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 + '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 + '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 + '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + dependencies: + '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + + '@astrojs/internal-helpers@0.10.2': + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + js-yaml: 4.3.1 + picomatch: 4.0.5 + retext-smartypants: 6.2.0 + shiki: 4.4.2 + smol-toml: 1.7.1 + unified: 11.0.5 + + '@astrojs/markdown-remark@7.2.2': + dependencies: + '@astrojs/internal-helpers': 0.10.2 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-smartypants: 3.0.3 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/markdown-satteri@0.3.5': + dependencies: + '@astrojs/internal-helpers': 0.10.2 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + satteri: 0.9.5 + + '@astrojs/prism@4.0.2': + dependencies: + prismjs: 1.30.0 + + '@astrojs/telemetry@3.3.3': + dependencies: + ci-info: 4.4.0 + dset: 3.1.4 + is-docker: 4.0.0 + package-manager-detector: 1.8.0 + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bruits/satteri-darwin-arm64@0.9.5': + optional: true + + '@bruits/satteri-darwin-x64@0.9.5': + optional: true + + '@bruits/satteri-linux-arm64-gnu@0.9.5': + optional: true + + '@bruits/satteri-linux-arm64-musl@0.9.5': + optional: true + + '@bruits/satteri-linux-x64-gnu@0.9.5': + optional: true + + '@bruits/satteri-linux-x64-musl@0.9.5': + optional: true + + '@bruits/satteri-wasm32-wasi@0.9.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@bruits/satteri-win32-arm64-msvc@0.9.5': + optional: true + + '@bruits/satteri-win32-x64-msvc@0.9.5': + optional: true + + '@capsizecss/unpack@4.0.1': + dependencies: + fontkitten: 1.0.3 + + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.2 + optional: true + + '@img/sharp-darwin-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.2 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.2': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.2': + optional: true + + '@img/sharp-linux-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.2 + optional: true + + '@img/sharp-linux-arm@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.2 + optional: true + + '@img/sharp-linux-ppc64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.2 + optional: true + + '@img/sharp-linux-riscv64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.2 + optional: true + + '@img/sharp-linux-s390x@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.2 + optional: true + + '@img/sharp-linux-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + optional: true + + '@img/sharp-wasm32@0.35.3': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.3': + dependencies: + '@img/sharp-wasm32': 0.35.3 + optional: true + + '@img/sharp-win32-arm64@0.35.3': + optional: true + + '@img/sharp-win32-ia32@0.35.3': + optional: true + + '@img/sharp-win32-x64@0.35.3': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oslojs/encoding@1.1.0': {} + + '@oxc-project/types@0.143.0': {} + + '@rolldown/binding-android-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.3': + optional: true + + '@rolldown/binding-darwin-x64@1.2.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.3': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/pluginutils@5.4.0': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + + '@shikijs/core@4.4.2': + dependencies: + '@shikijs/primitive': 4.4.2 + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + + '@shikijs/primitive@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.4.2': + dependencies: + '@shikijs/types': 4.4.2 + + '@shikijs/types@4.4.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.3': {} + + am-i-vibing@0.4.0: + dependencies: + process-ancestry: 0.1.0 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astro@7.1.6(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3): + dependencies: + '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/internal-helpers': 0.10.2 + '@astrojs/markdown-satteri': 0.3.5 + '@astrojs/telemetry': 3.3.3 + '@capsizecss/unpack': 4.0.1 + '@clack/prompts': 1.7.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.4.0 + am-i-vibing: 0.4.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + ci-info: 4.4.0 + clsx: 2.1.1 + common-ancestor-path: 2.0.0 + cookie: 2.0.1 + devalue: 5.9.0 + diff: 8.0.4 + dset: 3.1.4 + es-module-lexer: 2.3.1 + esbuild: 0.28.1 + flattie: 1.1.1 + fontace: 0.4.1 + get-tsconfig: 5.0.0-beta.4 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + js-yaml: 4.3.1 + jsonc-parser: 3.3.1 + magic-string: 1.1.0 + magicast: 0.5.4 + mrmime: 2.0.1 + neotraverse: 1.0.1 + obug: 2.1.4 + p-limit: 7.3.1 + p-queue: 9.3.3 + package-manager-detector: 1.8.0 + piccolore: 0.1.3 + picomatch: 4.0.5 + semver: 7.8.5 + shiki: 4.4.2 + smol-toml: 1.7.1 + svgo: 4.0.2 + tinyclip: 0.1.15 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + ultrahtml: 1.7.0 + unifont: 0.7.4 + unstorage: 1.17.5 + vite: 8.2.0(esbuild@0.28.1) + vitefu: 1.1.3(vite@8.2.0(esbuild@0.28.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 22.0.0 + zod: 4.4.3 + optionalDependencies: + '@astrojs/markdown-remark': 7.2.2 + sharp: 0.35.3 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@emnapi/core' + - '@emnapi/runtime' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - '@vitejs/devtools' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - uploadthing + - yaml + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + boolbase@1.0.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + ci-info@4.4.0: {} + + clsx@2.1.1: {} + + comma-separated-tokens@2.0.3: {} + + commander@11.1.0: {} + + common-ancestor-path@2.0.0: {} + + cookie-es@1.2.3: {} + + cookie@2.0.1: {} + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-tree@2.2.1: + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.1 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css-what@6.2.2: {} + + csso@5.0.5: + dependencies: + css-tree: 2.2.1 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + defu@6.1.7: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + devalue@5.9.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@8.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dset@3.1.4: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-module-lexer@2.3.1: {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escape-string-regexp@5.0.0: {} + + estree-walker@2.0.2: {} + + eventemitter3@5.0.4: {} + + extend@3.0.2: {} + + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + + fast-wrap-ansi@0.2.2: + dependencies: + fast-string-width: 3.0.2 + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + flattie@1.1.1: {} + + fontace@0.4.1: + dependencies: + fontkitten: 1.0.3 + + fontkitten@1.0.3: + dependencies: + tiny-inflate: 1.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@5.0.0-beta.4: + dependencies: + resolve-pkg-maps: 1.0.0 + + github-slugger@2.0.0: {} + + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.5 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.5 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.3 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.5 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.2.0: {} + + iron-webcrypto@1.2.1: {} + + is-docker@4.0.0: {} + + is-plain-obj@4.1.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + jsonc-parser@3.3.1: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + longest-streak@3.1.0: {} + + lru-cache@11.5.2: {} + + magic-string@1.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.4: + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdn-data@2.0.28: {} + + mdn-data@2.27.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.17: {} + + neotraverse@1.0.1: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-fetch-native@1.6.7: {} + + node-mock-http@1.0.5: {} + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + obug@2.1.4: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.4 + + ohash@2.0.11: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + p-limit@7.3.1: + dependencies: + yocto-queue: 1.2.2 + + p-queue@9.3.3: + dependencies: + eventemitter3: 5.0.4 + p-timeout: 7.0.1 + + p-timeout@7.0.1: {} + + package-manager-detector@1.8.0: {} + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + piccolore@0.1.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.5: {} + + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prismjs@1.30.0: {} + + process-ancestry@0.1.0: {} + + property-information@7.2.0: {} + + radix3@1.1.2: {} + + readdirp@5.0.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.5 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + unified: 11.0.5 + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.3: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + resolve-pkg-maps@1.0.0: {} + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.1.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + rolldown@1.2.3: + dependencies: + '@oxc-project/types': 0.143.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.3 + '@rolldown/binding-darwin-arm64': 1.2.3 + '@rolldown/binding-darwin-x64': 1.2.3 + '@rolldown/binding-freebsd-x64': 1.2.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.3 + '@rolldown/binding-linux-arm64-gnu': 1.2.3 + '@rolldown/binding-linux-arm64-musl': 1.2.3 + '@rolldown/binding-linux-ppc64-gnu': 1.2.3 + '@rolldown/binding-linux-s390x-gnu': 1.2.3 + '@rolldown/binding-linux-x64-gnu': 1.2.3 + '@rolldown/binding-linux-x64-musl': 1.2.3 + '@rolldown/binding-openharmony-arm64': 1.2.3 + '@rolldown/binding-win32-arm64-msvc': 1.2.3 + '@rolldown/binding-win32-x64-msvc': 1.2.3 + + satteri@0.9.5: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + optionalDependencies: + '@bruits/satteri-darwin-arm64': 0.9.5 + '@bruits/satteri-darwin-x64': 0.9.5 + '@bruits/satteri-linux-arm64-gnu': 0.9.5 + '@bruits/satteri-linux-arm64-musl': 0.9.5 + '@bruits/satteri-linux-x64-gnu': 0.9.5 + '@bruits/satteri-linux-x64-musl': 0.9.5 + '@bruits/satteri-wasm32-wasi': 0.9.5 + '@bruits/satteri-win32-arm64-msvc': 0.9.5 + '@bruits/satteri-win32-x64-msvc': 0.9.5 + + sax@1.6.1: {} + + semver@7.8.5: {} + + sharp@0.35.3: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.3 + '@img/sharp-darwin-x64': 0.35.3 + '@img/sharp-freebsd-wasm32': 0.35.3 + '@img/sharp-libvips-darwin-arm64': 1.3.2 + '@img/sharp-libvips-darwin-x64': 1.3.2 + '@img/sharp-libvips-linux-arm': 1.3.2 + '@img/sharp-libvips-linux-arm64': 1.3.2 + '@img/sharp-libvips-linux-ppc64': 1.3.2 + '@img/sharp-libvips-linux-riscv64': 1.3.2 + '@img/sharp-libvips-linux-s390x': 1.3.2 + '@img/sharp-libvips-linux-x64': 1.3.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.2 + '@img/sharp-libvips-linuxmusl-x64': 1.3.2 + '@img/sharp-linux-arm': 0.35.3 + '@img/sharp-linux-arm64': 0.35.3 + '@img/sharp-linux-ppc64': 0.35.3 + '@img/sharp-linux-riscv64': 0.35.3 + '@img/sharp-linux-s390x': 0.35.3 + '@img/sharp-linux-x64': 0.35.3 + '@img/sharp-linuxmusl-arm64': 0.35.3 + '@img/sharp-linuxmusl-x64': 0.35.3 + '@img/sharp-webcontainers-wasm32': 0.35.3 + '@img/sharp-win32-arm64': 0.35.3 + '@img/sharp-win32-ia32': 0.35.3 + '@img/sharp-win32-x64': 0.35.3 + optional: true + + shiki@4.4.2: + dependencies: + '@shikijs/core': 4.4.2 + '@shikijs/engine-javascript': 4.4.2 + '@shikijs/engine-oniguruma': 4.4.2 + '@shikijs/langs': 4.4.2 + '@shikijs/themes': 4.4.2 + '@shikijs/types': 4.4.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + sisteransi@1.0.5: {} + + smol-toml@1.7.1: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + svgo@4.0.2: + dependencies: + commander: 11.1.0 + css-select: 5.2.2 + css-tree: 3.2.1 + css-what: 6.2.2 + csso: 5.0.5 + picocolors: 1.1.1 + sax: 1.6.1 + + tiny-inflate@1.0.3: {} + + tinyclip@0.1.15: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tslib@2.8.1: + optional: true + + ufo@1.6.4: {} + + ultrahtml@1.7.0: {} + + uncrypto@0.1.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unifont@0.7.4: + dependencies: + css-tree: 3.2.1 + ofetch: 1.5.1 + ohash: 2.0.11 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unstorage@1.17.5: + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.11 + lru-cache: 11.5.2 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.4 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.2.0(esbuild@0.28.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.3 + tinyglobby: 0.2.17 + optionalDependencies: + esbuild: 0.28.1 + fsevents: 2.3.3 + + vitefu@1.1.3(vite@8.2.0(esbuild@0.28.1)): + optionalDependencies: + vite: 8.2.0(esbuild@0.28.1) + + web-namespaces@2.0.1: {} + + xxhash-wasm@1.1.0: {} + + yargs-parser@22.0.0: {} + + yocto-queue@1.2.2: {} + + zod@4.4.3: {} + + zwitch@2.0.4: {} diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml new file mode 100644 index 00000000..49c0ad74 --- /dev/null +++ b/docs/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: false diff --git a/docs/src/pages/guide/components.md b/docs/src/pages/guide/components.md index b45f9bce..e8a021e4 100644 --- a/docs/src/pages/guide/components.md +++ b/docs/src/pages/guide/components.md @@ -2,212 +2,162 @@ layout: ../../layouts/Guide.astro title: Building Components slug: components -description: "Components are the basic execution units in vihaco — an instruction type, an optional message, an optional effect, and one #[component(...)] impl that executes the instruction." +description: "Declare reusable runtime components with component!, define instruction products, and implement Execute per instruction." --- # Building Components With `vihaco` -Components are the basic execution units in `vihaco`. -You define: +A component owns state and the behavior for one or more runtime instruction +products. The component declaration and the execution implementation are two +deliberate boundaries: -- an instruction type -- an optional resolved message type -- an optional effect type -- one `#[component(...)]` impl that executes the instruction +- `component!` declares the state type and the instruction product types. +- `Execute` implements one product `I`, with its own message, effect, and + fault types. -This guide shows the current public authoring model for defining your own component. +This lets a component expose operations with different input and output +contracts without forcing them through one large instruction enum. -If you want a focused guide to instruction enums, explicit instruction width, and nested composite-level wrappers, read [Defining Instructions With `vihaco`](/guide/instructions). -If you want a focused guide to resolved execution input and composite-side message generation, read [Using Messages With `vihaco`](/guide/messages). - -## The Core Pieces - -A component usually starts with two or three data types: - -- an instruction enum with `#[derive(Instruction)]` -- a message type with `#[derive(Message)]` when execution needs pre-resolved input -- one or more plain Rust effect types when execution needs to return output - -Use them this way: - -- `Instruction`: the operation the component should execute -- `Message`: resolved execution input delivered into the component for that step -- `Effect`: value returned from execution and later interpreted by the runtime or delivered to observers - -Example: +## Declare a component ```rust -use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; +use vihaco::component; -#[derive(Debug, Clone, Instruction)] -pub enum CounterInst { - Add(i64), - Print, -} - -#[derive(Debug, Clone)] -pub struct PrintPrefix(pub String); - -impl Message for PrintPrefix {} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct StdoutEffect(pub String); - -#[derive(Debug, Default)] -pub struct Counter { - value: i64, -} -``` +component! { + component Counter { + value: i64, + } -## Defining `#[component(...)]` - -Component execution lives on an impl block annotated with `#[component(...)]`. - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, Instruction, Message, component}; -# #[derive(Debug, Clone, Instruction)] -# pub enum CounterInst { Add(i64), Print } -# #[derive(Debug, Clone, Message)] -# pub struct PrintPrefix(pub String); -# #[derive(Debug, Clone, PartialEq, Eq)] -# pub struct StdoutEffect(pub String); -# #[derive(Debug, Default)] -# pub struct Counter { value: i64 } -#[component(instruction = CounterInst, message = PrintPrefix, effect = StdoutEffect)] -impl Counter { - fn execute(&mut self, inst: CounterInst, msg: PrintPrefix) -> Result> { - match inst { - CounterInst::Add(v) => { - self.value += v; - Ok(Effects::none()) - } - CounterInst::Print => Ok(Effects::one(StdoutEffect(format!( - "{}{}", - msg.0, self.value - )))), - } + instruction { + Add(i64), + Print, } } ``` -The execution method shape is: - -```rust ignore -fn execute(&mut self, inst: Inst, msg: Msg) -> eyre::Result> -``` +The macro creates `counter::Counter` and places the products in +`counter::instruction`: `Add(i64)` and `Print`. Named and tuple products are +also supported: -Important points: +```rust +use vihaco::component; -- `Inst` must match the `instruction = ...` type -- `Msg` must match the `message = ...` type -- when `effect = ...` is omitted, the effect type defaults to `()` -- normal execution output is returned as `Effects` +component! { + component RegisterFile { + values: Vec, + } -It is useful to keep the data flow straight: + instruction { + Read { slot: usize }, + Write(usize, i64), + Reset, + } +} +``` -- `Message` goes into a component -- `Effect` comes out of a component -- components consume `Message` -- runtimes and observers consume `Effect` +The declaration is a catalog of runtime products. It does not define source +syntax, assign machine-wide device codes, or choose which products a composite +exposes. -## When To Use `message = ()` +## Implement `Execute` -Use `message = ()` when the component can execute directly from its instruction and local state. +Execution is implemented per product. `Message` is a marker for owned, +runtime-supplied input; `NoMessage` is the standard input for an instruction +that needs none. `StepResult` keeps returned effects separate from whether the +operation completed or parked. -```rust ignore +```rust use eyre::Result; -use vihaco::{Effects, Instruction, component}; +use vihaco::{ + component, Effects, Execute, Execution, Message, StepResult, +}; -#[derive(Debug, Clone, Instruction)] -pub enum LampInst { - On, - Off, +component! { + component Counter { + value: i64, + } + + instruction { + Add(i64), + Print, + } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct LampChanged(pub bool); +#[derive(Debug, Clone)] +pub struct Prefix(String); +impl Message for Prefix {} -#[derive(Debug, Default)] -pub struct Lamp { - on: bool, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Line(String); + +impl Execute for counter::Counter { + type Message = (); + type Effect = (); + type Fault = eyre::Report; + + fn execute( + &mut self, + instruction: &counter::instruction::Add, + _message: (), + ) -> Result, Self::Fault> { + self.value += instruction.0; + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } } -#[component(instruction = LampInst, message = (), effect = LampChanged)] -impl Lamp { - fn execute(&mut self, inst: LampInst, _msg: ()) -> Result> { - self.on = matches!(inst, LampInst::On); - Ok(Effects::one(LampChanged(self.on))) +impl Execute for counter::Counter { + type Message = Prefix; + type Effect = Line; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &counter::instruction::Print, + message: Prefix, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::one(Line(format!("{}{}", message.0, self.value))), + execution: Execution::Complete, + }) } } ``` -Use a non-unit message when execution needs resolved data that should not be encoded directly in the instruction itself. - -As a rule: - -- use `Message` for step-local execution input -- use `Effect` for values the runtime should interpret or deliver after execution - -## Execution Surface - -Component execution depends only on explicit inputs and returned effects. - -- `Instruction` and `Message` are the full inputs to `execute(...)` -- `Effects` is the full output from `execute(...)` -- runtimes decide how to interpret returned effects after execution +The `Execute` contract is: -## Design Guidance - -- Put bytecode-visible execution variants in the instruction enum. -- Put resolved execution input in the message type. -- Put follow-up outputs in plain effect types. -- Keep the component responsible for its own state mutation. -- Use `effect = StepOutcome` when a component needs to return control-flow signals. - -## Returning A Custom Effect - -By default, `execute(...)` returns `Result>`. When a component needs to return a real effect, use the `effect` parameter: - -```rust ignore -use vihaco::{Effects, Instruction, Message, component}; -use vihaco_cpu::StepOutcome; - -#[derive(Debug, Clone, Instruction)] -pub enum CpuInst { - Nop, - Halt, -} - -#[derive(Debug, Clone, Message)] -pub struct CpuMsg; - -pub struct CpuCore; - -#[component(instruction = CpuInst, message = CpuMsg, effect = StepOutcome)] -impl CpuCore { - fn execute(&mut self, inst: CpuInst, _msg: CpuMsg) -> eyre::Result> { - match inst { - CpuInst::Nop => Ok(Effects::one(StepOutcome::Continue)), - CpuInst::Halt => Ok(Effects::one(StepOutcome::Halt)), - } - } -} +```text +Execute::execute(&mut self, &I, Message) + -> Result, Fault> ``` -The `effect` parameter is optional. When omitted, the macro sets `type Effect = ()`. When present, the component's `GeneratedComponent::Effect` type matches what you specify. +`Effects` can contain zero, one, or many values. `Execution::Complete` tells a +parent that it may advance its program counter; `Execution::Parked` tells it +to retain the operation until an owned completion is available. The runtime +does not infer timing or scheduling from this value. + +## Capabilities around execution -**Important:** effects only matter when some runtime continues them. In practice: +Components can expose reusable capabilities independently of instruction +execution: -- Hand-written runtime code can call `execute_generated` directly and extract the returned effects. For single-effect control flow, `expect_exactly_one_effect(...)` is the common helper. -- When a runtime needs to mix control-flow effects with other follow-ups, it usually defines a runtime-local sum-effect enum, gathers those values, and continues them in one place. -- Transitional `#[composite]` wiring generates the device dispatch and metadata; continuing returned effects to observers is something the hand-written runtime does (see [Defining A Composite With `vihaco`](/guide/composites)), and it does not interpret `StepOutcome` for you. +- `Supply` produces an owned message, often from a stack or queue. +- `Absorb` consumes an owned effect, often by updating state. +- `Observe` borrows an effect for diagnostics, tracing, or recording. +- `Handle` is the composite-selected route for the one consumer that + receives ownership of an effect. -As a rule: use plain effect types for observer-delivered outputs, and use runtime-local sum-effect enums when a hand-written runtime needs extra per-step interpretation. +These contracts keep a reusable component independent of the composite that +contains it. The composite decides which capability is used on each route. -## What Comes Next +## Planned extensions -Once you have one or more components, the next step is to understand how observer types consume the returned effects. +The current runtime leaves resume/continuation dispatch and timing policy to +ordinary Rust in the parent runtime. A future macro layer is planned to make +those boundaries more convenient; until then, examples should implement them +explicitly and should treat `Execution::Parked` as a real runtime state. -Continue with [Observing Effects With `#[observe]`](/guide/observers). +Continue with [Defining Composites](/guide/composites) and +[Using Messages](/guide/messages). diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index a5be9559..9530f706 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -2,373 +2,140 @@ layout: ../../layouts/Guide.astro title: Defining a Composite slug: composites -description: "Composite structs are the composition root in vihaco — #[composite] generates the outer instruction enum and device metadata; message resolution and effect delivery are hand-written." +description: "Compose components with composite!, select runtime routes, resolve messages, and deliver effects." --- -# Defining A Composite With `vihaco` +# Defining a Composite With `vihaco` -Composite structs are the composition root in `vihaco`. -They own components, observers, and device codes, and they are where the -generated wiring meets your hand-written runtime. +A composite is the machine-specific composition root. It owns component +instances and declares the routes that connect a public machine instruction to +a component product, a message source, observers, and one effect handler. -This guide shows how to wire components and observers into a composite using the current macro surface. - -If you have not read the observer guide yet, read [Observing Effects With `#[observe]`](/guide/observers) first. -For a focused guide to composite-side message resolution before component execution, read [Using Messages With `vihaco`](/guide/messages). - -## A Small Composite - -Assume you already have: - -- a component such as `Counter` -- an effect type such as `StdoutEffect` -- a type that observes that effect +## A routed composite ```rust use eyre::Result; -use vihaco::{Effects, Observe}; - -#[derive(Debug, Clone)] -pub struct StdoutEffect(pub String); - -#[derive(Debug, Default)] -pub struct StdoutCollector { - lines: Vec, +use vihaco::{ + composite, Absorb, Effects, Execute, Execution, Message, Observe, StepResult, + Supply, +}; + +struct Stack(Vec); +impl Supply<(i64, i64)> for Stack { + type Fault = eyre::Report; + fn supply(&mut self) -> Result<(i64, i64), Self::Fault> { + let rhs = self.0.pop().ok_or_else(|| eyre::eyre!("underflow"))?; + let lhs = self.0.pop().ok_or_else(|| eyre::eyre!("underflow"))?; + Ok((lhs, rhs)) + } } -impl Observe for StdoutCollector { - type Effect = (); - type Error = eyre::Report; - - fn observe(&mut self, effect: &StdoutEffect) -> Result> { - self.lines.push(effect.0.clone()); - Ok(Effects::none()) +#[derive(Clone)] +struct Add; +struct Arithmetic; +struct Value(i64); +impl Execute for Arithmetic { + type Message = (i64, i64); + type Effect = Value; + type Fault = eyre::Report; + fn execute(&mut self, _: &Add, (lhs, rhs): (i64, i64)) -> Result> { + Ok(StepResult { + effects: Effects::one(Value(lhs + rhs)), + execution: Execution::Complete, + }) } } -``` - -Now you can compose a runtime root: - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, Instruction, Observe, component}; -# #[derive(Debug, Clone, Instruction)] -# pub enum CounterInst { Print } -# #[derive(Debug, Default)] -# pub struct Counter; -# #[component(instruction = CounterInst, message = ())] -# impl Counter { -# fn execute(&mut self, _inst: CounterInst, _msg: ()) -> Result> { Ok(Effects::none()) } -# } -# #[derive(Debug, Clone)] -# pub struct StdoutEffect(pub String); -# #[derive(Debug, Default)] -# pub struct StdoutCollector { lines: Vec } -# impl Observe for StdoutCollector { -# type Effect = (); -# type Error = eyre::Report; -# fn observe(&mut self, effect: &StdoutEffect) -> Result> { -# self.lines.push(effect.0.clone()); -# Ok(Effects::none()) -# } -# } -use vihaco::composite; - -#[composite] -#[derive(Debug, Default)] -pub struct CounterComposite { - #[device(0x00, alias = "count")] - counter: Counter, - - // A plain observer field — the runtime delivers StdoutEffect to it. - stdout: StdoutCollector, +impl Absorb for Stack { + type Fault = eyre::Report; + fn absorb(&mut self, value: Value) -> Result<()> { self.0.push(value.0); Ok(()) } } -``` - -## What `#[composite]` Generates - -`#[composite]` is transitional scaffolding that generates the repetitive composition glue from the `#[device(...)]` fields: - -- **An outer instruction enum** named `Instruction`, with one variant per device field. Each variant is the PascalCase of the field name and wraps that component's instruction type. For `CounterComposite` above the macro emits, roughly: - - ```rust ignore - #[derive(Debug, Clone, Instruction)] - pub enum CounterCompositeInstruction { - Counter(::Instruction), - } - ``` - -- **Composite metadata** — an `impl GeneratedMachine` whose `metadata()` returns a `CompositeMetadata` listing each device's code and field name, plus the source-symbol aliases (so a loader can map a name like `"counter"` to its device code). - -- **Section loading glue** — `LoadBytecodeSection` and `LoadSstSection` impls that call your own-section loader for the composite's own section, then route direct child sections to `#[loadable]` devices. - -The `#[device]` and `#[loadable]` attributes are stripped from the struct the macro emits, so they don't leak into your type. - -The long-term model is still explicit Rust composition. The macro is convenience for the device dispatch and metadata, not the semantic center of the design — message resolution and effect delivery stay in hand-written runtime code. - -## The Field Attributes - -### `#[device(CODE, alias = "…")]` - -Associates a component field with a device code and optional source aliases. - -```rust ignore -#[device(0x00, alias = "count")] -counter: Counter, -``` - -- `CODE` is a `u8` device code; it must be unique across the composite (a duplicate is a compile error). -- `alias = "…"` registers a source-symbol alias for the field; you can repeat it for multiple aliases. The field name itself is always registered as a source symbol, and every name (field or alias) must be unique across the composite. - -The field type must implement `GeneratedComponent` (which `#[component(...)]` provides). The device code and aliases are what a loader uses to validate source symbols and route instructions when a composite loads a module. - -### Own Section Loading - -Headers, program streams, and program-counter delegation are ordinary Rust. Implement `LoadOwnBytecodeSection` or `LoadOwnSstSection` for the composite to load the current section's own data: - -```rust ignore -#[derive(Default)] -pub struct CpuHeader { - cores: u32, +struct Trace; +impl Observe for Trace { + type Effect = (); + type Error = eyre::Report; + fn observe(&mut self, _: &Value) -> Result> { Ok(Effects::none()) } } -#[composite] -#[derive(Default)] -pub struct CpuMachine { - info: CpuHeader, - program: vihaco::ProgramImage, -} +composite! { + composite Calculator { + error = eyre::Report; -impl vihaco::LoadOwnBytecodeSection for CpuMachine { - fn load_own_bytecode_section<'a>( - &mut self, - section: vihaco::BytecodeSectionView<'a, MyContext>, - ) -> eyre::Result<()> { - self.info = section.decode_header::()?; - self.program.module.code = section.decode_instructions()?; - self.program.context = Some(section.context_handle()); - self.program.pc = 0; - Ok(()) + #[device(0x01, alias = "alu")] + arithmetic: Arithmetic, + stack: Stack, + trace: Trace, } -} -``` -For SST, parse the section into `vihaco::syntax::ParsedModule` with `ParsedModule::::parse_section(section)`, then lower it with your `Resolve` impl into a `Module`/`ProgramImage`. `Ty` is the consumer-provided source type syntax. If a composite drives an instruction pointer, implement `ProgramCounter` manually by delegating to the field that owns it. - -For structural composites that only route child sections, make that no-op explicit: - -```rust ignore -impl vihaco::LoadOwnBytecodeSection for Machine { - fn load_own_bytecode_section<'a>( - &mut self, - _section: vihaco::BytecodeSectionView<'a, C>, - ) -> eyre::Result<()> { - Ok(()) + runtime_instructions { + Add(Add) => arithmetic { + message from stack; + effects { + observe trace; + absorb with stack; + } + } } } ``` -### `#[loadable]` - -Marks a device field that should receive its own direct child section when loading v1 multi-section bytecode or SST. The device owns its loader internally; the generated composite loader only routes the section to the marked device's concrete load impl. - -```rust ignore -#[composite] -#[derive(Default)] -pub struct Machine { - #[device(0x01)] - #[loadable("signal")] - signal: SignalMachine, -} -``` - -`#[loadable]` must be used on a `#[device(...)]` field whose type implements the loader trait for the representation you are loading as well as `GeneratedComponent`. Binary bytecode delegates through `LoadBytecodeSection`; SST delegates through `LoadSstSection`. The attribute uses the field name as the local section name. `#[loadable("name")]` overrides it. Names must be non-empty direct child names, so they cannot contain `/`. +The generated public `CalculatorInstruction::Add(Add)` is the machine-local +runtime sum. `execute_generated` resolves the message, calls +`Execute`, invokes observers in declaration order, and passes each effect +to exactly one handler. `absorb with stack` delegates to `Stack::absorb`; use +`handle with method` when routing policy belongs to the composite. -Section identity is represented as a `SectionPath`, which is a vector of resolved local section names. The root section is `SectionPath::root()` with zero components. A root child named `cpu` has the path `cpu`; a child named `alu` inside that section has `cpu/alu`. Generated loading asks the current section for direct children by local name, so the same composite can be loaded at the root or under another parent section. +## Route clauses -Manual loaders can inspect a section through the concrete view type for the format being loaded: `BytecodeSectionView<'bc, C>` for bytecode and `SstSectionView<'bc, C>` for SST. Both expose `child(name)` for a direct child and `children()` for all direct children; start from `BytecodeFile::root()` or `SstFile::root()` to inspect an entire file. - -The generated loader is strict: - -- any present direct child section must correspond to a `#[loadable]` device field -- `#[loadable]` device fields are optional; if a file omits that child section, the device is left unchanged -- the composite's own section header/body is loaded only through `LoadOwnBytecodeSection` or `LoadOwnSstSection` -- manual loaders can inspect bytecode headers through `BytecodeSectionView::header_bytes()` / `BytecodeSectionView::decode_header::()`, or SST headers through `SstSectionView::header_text()` / `SstSectionView::parse_header::()` - -## Multi-Section Bytecode - -The read-side bytecode API lives in `vihaco` and `vihaco::loader`. - -```rust ignore -fn load_machine<'bc>( - file: &'bc vihaco::BytecodeFile, -) -> eyre::Result { - let mut machine = Machine::default(); - machine.load_bytecode_section(file.root())?; - Ok(machine) -} - -let file: vihaco::BytecodeFile = vihaco::BytecodeFile::from_bytes(bytes)?; -let machine = load_machine(&file)?; -``` - -The v1 file layout is: - -```text -VHBC magic -u16 version = 1 -u16 flags = 0 -u64 global_context_len -global context bytes -root section bytes -``` - -The bytecode global context is interpreted by the file's `C: BytecodeGlobalContext`. For bytecode, `C` must also implement `SectionNameResolver`, because child section table entries store local section names indirectly by ID. SST global contexts only implement `SstGlobalContext`; section names are present directly in the text. Program tables such as constants, functions, labels, entrypoints, and source symbols are architecture-specific and should live in a section header, `Module` fields, `Module::extra`, or a custom loader-owned data structure. - -Each section is: - -```text -section frame: -u64 section_len -u64 header_len -composite header bytes -section bytecode header: -u64 bytecode_len -bytecode bytes -child section table header: -u32 child_count -child table entries -child section bytes -``` - -Each child table entry stores: +Every route names a payload and target: ```text -u32 local_name_string -u64 section_offset -``` - -The section frame is part of every section, including the root section. The bytecode header starts after the composite header, and the section's bytecode immediately follows that length. Child-related metadata comes after the parent bytecode. `local_name_string` is resolved through `GlobalContext::section_name` and represents the child's local section name. The parser builds each child's `SectionPath` by appending that resolved name to the parent path. Child section offsets are relative to the start of the containing section. - -### SST Multi-Section Bytecode - -SST uses `SstFile`, `SstSectionView<'bc, C>`, and generated `LoadSstSection` machinery. The backing contents are the original SST and each section stores ranges into that string. Use `SstFile` for an empty global block, or `SstFile` when you need a custom context: - -```rust ignore -let file: vihaco::SstFile = - vihaco::SstFile::::from_text(source)?; - -let file: vihaco::SstFile = - vihaco::SstFile::::from_text(source)?; - -let mut machine = Machine::default(); -machine.load_sst_section(file.root())?; -``` - -The file begins with the text magic/version marker, then a global context block: - -```text -sst v1 - -.global: -global context text -.global. -``` - -`sst v1` is the text spelling of version 1. With `NoContext`, the global block must be empty. For `SstFile`, the context body is delegated to `C::from_text(context_text)`; custom SST formats usually provide a custom `SstGlobalContext` that interprets this block. The context start marker `.global:` and end marker `.global.` must be at indentation level 0. - -After the context comes the root section. Sections use `.section(name):` to begin and `.section(name).` to end. The top-level section must be named `root` (`.section(root):` and `.section(root).`), and it is parsed as `SectionPath::root()`. Direct child section names become path components. - -```text -.section(root): - .header(root): - root header - .header(root). - - .text(root): - fn @main() { - root instructions - } - .text(root). - - .section(cpu): - .text(cpu): - fn @main() { - cpu instructions - } - .text(cpu). - .section(cpu). -.section(root). -``` - -Inside a section: - -- `.header(name):` / `.header(name).` delimit the composite header text for `SstSectionView::header_text()` -- `.text(name):` / `.text(name).` delimit the section bytecode text for `SstSectionView::sst()` -- child sections are nested directly inside their parent section -- header, bytecode, and direct child section markers must be indented with exactly one tab more than their parent section -- section end markers must use the same indentation as their matching section start marker -- section names must be local names; `/` is rejected in a child marker name - -The SST parser preserves the original header and bytecode ranges, including their leading tabs. Load section programs by mapping the section to `ParsedModule::::parse_section(section)`, then run your `Resolve` impl to produce the runtime `Module`. Both the instruction type `I` and source type `Ty` must implement `vihaco_parser::Parse`. - -`ProgramImage` is the standard in-memory program image. Machines load binary bytecode into it by decoding `section.bytecode()` with `BytecodeSectionView::decode_instructions::()` and, when useful, storing the section's cloned `ContextHandle`. For SST, resolve a `ParsedModule` into the image's `module`. `ProgramImage` implements `ProgramCounter` and exposes functions, strings, and constants through `GetProgramInfo` from its own `module` fields. - -## Effect Continuation Is Hand-Written - -`#[composite]` generates the instruction enum and metadata, but it does **not** auto-deliver effects to observers. Continuing effects is something the runtime does explicitly: execute a component, then hand each returned effect to the types that observe it by calling their `Observe` impls. - -```rust ignore -use vihaco::{GeneratedComponent, Observe}; - -impl CounterComposite { - fn print(&mut self, msg: PrintPrefix) -> eyre::Result<()> { - // Counter executes Print and returns a StdoutEffect. - let effects = self.counter.execute_generated(CounterInst::Print, msg)?; - // Deliver each effect to the observer that handles it. - for effect in effects { - Observe::::observe(&mut self.stdout, &effect)?; - } - Ok(()) +Variant(Payload) => field { + message none; + effects { + observe observer_a, observer_b; + handle with composite_method; } } ``` -Conventions to follow when you write that delivery: +Message sources are deliberately explicit: -- components return `Effects` -- the runtime continues those effects to all matching observer fields -- both standalone observers and components that also observe receive effects through the same `Observe::observe` call -- follow-up effects continue depth-first -- `Effects::Many(...)` is continued left-to-right -- if an observer needs more data, stage it into a richer effect instead of relying on delivery context +- `message none` passes `NoMessage`. +- `message from field` calls `Supply` on that field. +- `message with method` calls a composite method with the instruction payload. -## Hand-Written Runtimes +Effect handlers are exclusive: -Not every runtime uses a generic step loop. Hand-written runtimes often call `execute_generated(...)` directly, extract the returned effects, and then interpret or re-deliver them themselves. +- `absorb with field` calls `Absorb` on a component field. +- `handle with method` calls a composite method with owned `E`. -The common pattern is: +The declared `error = E` type is the normalization boundary for component, +message, observer, and handler failures. -- use `effect = StepOutcome` when a component's direct output is control flow -- define a runtime-local sum-effect enum when a step needs to mix control flow with other follow-up values -- continue that runtime-local effect set in one place, forwarding observer-facing effects as needed +## Devices and loading -## Design Guidance +`#[device(code, alias = "name")]` contributes device metadata and source-symbol +aliases. Codes must be unique. `#[loadable]` marks a device that receives a +direct child bytecode/SST section through the generated loader. A composite +that owns program data implements `LoadOwnBytecodeSection` or +`LoadOwnSstSection` in ordinary Rust. -- Keep the composite struct explicit and readable. -- Put `#[observe]` on the type that actually consumes the effect. -- Use `#[device(...)]` aliases that match your source model. -- Implement `LoadOwnBytecodeSection` / `LoadOwnSstSection` for headers and program streams owned by the current section. -- Implement `ProgramCounter` manually when the composite drives an instruction pointer. -- Prefer staged effect types over hidden cross-component observer context. -- Let generated code own the device dispatch and metadata; keep effect delivery and message resolution in one clear place in your runtime. +The composite macro can also declare structural composites with no +`runtime_instructions` block. Those composites still provide fields, device +metadata, and section wiring, while their event loop or parent dispatch remains +hand-written. -## What Comes Next +## Runtime boundaries -At this point you have the core authoring model: +The macro does not fetch instructions, own a program counter, generate a clock, +or generate continuation/resume dispatch. A runtime root can call +`execute_generated`, inspect `Execution`, update its own program state, and +schedule the next owned event. The demo shows this pattern with a CPU child and +a global event loop. -- components execute instructions -- `#[observe]` reacts to delivered effects -- composites generate the device wiring; the runtime resolves messages and continues effects +Those conveniences are planned for a later API extension. Documentation and +examples that need timing or parked operations should continue to show the +explicit parent-owned loop until that extension is implemented. -From here, the next useful step is to apply the same structure to your own domain types and source model. +See [Building Components](/guide/components), [Using Messages](/guide/messages), +and [Observing Effects](/guide/observers) for the individual contracts. diff --git a/docs/src/pages/guide/index.md b/docs/src/pages/guide/index.md index 4e161a22..48421dd1 100644 --- a/docs/src/pages/guide/index.md +++ b/docs/src/pages/guide/index.md @@ -30,10 +30,8 @@ For the type-by-type API reference, see the generated [rustdoc](/reference). 3. [Using Messages With `vihaco`](/guide/messages) How a runtime resolves execution input and supplies messages to components. 4. [Building Components With `vihaco`](/guide/components) - Connect instructions, messages, effects, and `#[component(...)]`. -5. [Observing Effects With `#[observe]`](/guide/observers) - How `#[observe]` works — on standalone observers and on components that also - react to effects. + Declare instruction products with `component!` and implement `Execute`. +5. [Observing Effects With `Observe`](/guide/observers) + Borrow effects for diagnostics while a route sends ownership to one handler. 6. [Defining A Composite With `vihaco`](/guide/composites) - Compose components and observers with the transitional `#[composite]` - wiring. + Compose components with `composite!` routes, message sources, and handlers. diff --git a/docs/src/pages/guide/instructions-advanced.md b/docs/src/pages/guide/instructions-advanced.md index 76d85279..aa7a598d 100644 --- a/docs/src/pages/guide/instructions-advanced.md +++ b/docs/src/pages/guide/instructions-advanced.md @@ -123,7 +123,8 @@ This keeps composition straightforward: - the machine exposes one outer instruction type - the wrapper enum handles outer dispatch without forcing every inner instruction type to be rewritten -> When you use the [`#[composite]`](/guide/composites) attribute, this outer wrapper enum is generated for you (as `Instruction`). Writing it by hand, as above, is the same shape — useful when you want full control over the wrapper. +> A `composite!` declaration generates the machine-local runtime sum as +> `Instruction`, with one explicitly declared route per product. ## How Nested Widths Compose @@ -172,6 +173,7 @@ This is what makes nested instruction composition deterministic: `#[derive(Instruction)]` covers bytecode and runtime semantics; source-text parsing is owned by an orthogonal `#[derive(vihaco_parser_derive::Parse)]` on the same enum. See [Pattern Parser Integration for Component Instructions](/guide/parser) for the parser-side workflow and [Module Parsing and Resolution](/guide/parser-advanced) for section headers, typed function bodies, and module resolution. -After defining an instruction type, the next step is usually to attach it to a component impl with `#[component(...)]`. +After defining an instruction type, implement `Execute` for the relevant +component product and select it from a `composite!` route. See [Building Components With `vihaco`](/guide/components) for the execution side of that model. diff --git a/docs/src/pages/guide/instructions.md b/docs/src/pages/guide/instructions.md index 88fc5cb0..1d789483 100644 --- a/docs/src/pages/guide/instructions.md +++ b/docs/src/pages/guide/instructions.md @@ -7,8 +7,9 @@ description: "How to define a component-local instruction enum with #[derive(Ins # Defining Instructions With `vihaco` -Instruction types are the bytecode-visible operations in `vihaco`. -They are usually Rust enums annotated with `#[derive(Instruction)]`. +Instruction types are the encoded operations in `vihaco`. Component runtime +products are declared with `component!`; source- or bytecode-facing enums can +derive `Instruction` when a single enum is the representation you want. This guide shows: @@ -47,7 +48,7 @@ CounterInst::Print => [opcode for Print] By default, `#[derive(Instruction)]` assigns opcodes in variant order starting at `0`. That means the first variant gets opcode `0`, the second gets `1`, and so on. -In normal component code, this instruction type is the `instruction = ...` value on the component impl: +For the current component model, declare products directly: ```rust ignore use eyre::Result; @@ -64,12 +65,9 @@ pub struct Lamp { on: bool, } -#[component(instruction = LampInst, message = ())] -impl Lamp { - fn execute(&mut self, inst: LampInst, _msg: ()) -> Result> { - self.on = matches!(inst, LampInst::On); - Ok(vihaco::Effects::none()) - } +component! { + component Lamp { on: bool, } + instruction { On, Off, } } ``` @@ -127,6 +125,7 @@ For explicit opcode assignment, explicit widths, and machine-level wrapper instr `#[derive(Instruction)]` covers bytecode and runtime semantics; source-text parsing is owned by an orthogonal `#[derive(vihaco_parser_derive::Parse)]` on the same enum. See [Pattern Parser Integration for Component Instructions](/guide/parser) for the parser-side workflow and [Module Parsing and Resolution](/guide/parser-advanced) for section headers, typed function bodies, and module resolution. -After defining an instruction type, the next step is usually to attach it to a component impl with `#[component(...)]`. +After defining products, implement `Execute` for each product as described in +[Building Components](/guide/components). See [Building Components With `vihaco`](/guide/components) for the execution side of that model. diff --git a/docs/src/pages/guide/messages.md b/docs/src/pages/guide/messages.md index bbf2f216..e6155682 100644 --- a/docs/src/pages/guide/messages.md +++ b/docs/src/pages/guide/messages.md @@ -2,368 +2,69 @@ layout: ../../layouts/Guide.astro title: Using Messages slug: messages -description: Message is the resolved execution input for a component — how a crate author defines a message type and how a composite resolves and supplies it during execution. +description: "Resolve owned execution input at a composite route and pass it to Execute." --- # Using Messages With `vihaco` -`Message` is the resolved execution input for a component. +A message is owned, runtime-supplied input for one instruction execution. It +is separate from the instruction payload so a source program can name an +operation while the composite supplies current machine state, timing data, +capabilities, or values from another component. -This is the key logic to keep in mind: - -- instructions tell a component what operation to perform -- messages provide execution input that the composite runtime resolves or generates -- effects are returned after execution and later interpreted by the runtime or delivered to observers - -That means: - -- components consume `Message` -- composites resolve or build `Message` -- observers do not consume `Message` -- observers do not receive a separate delivery context; any extra data should be staged into effects or owned locally - -This guide focuses on both sides of that contract: - -- how a crate author defines a message type -- how a composite author resolves and supplies messages during execution - -If you have not read the instruction guide yet, start with [Defining Instructions With `vihaco`](/guide/instructions). - -## What A Message Is For - -Use a message when a component needs step-local execution input that should not live directly in the instruction encoding. - -For example, a composite runtime may need to: - -- look up runtime state -- pop values from a stack -- derive timing information -- validate access to a device before execution - -The composite can do that work first, then pass the result into the component as a message. - -That keeps responsibilities clean: - -- `Instruction` is the bytecode-visible request -- `Message` is the resolved input to execute that request -- `Effect` is the value returned after execution - -## A Small Message Type - -Message types are usually plain Rust types with an explicit `Message` marker implementation. +Message types are ordinary Rust types. Implement the marker when the type is a +meaningful runtime message: ```rust use vihaco::Message; -#[derive(Debug, Clone)] -pub struct PlayMsg { - pub when_ns: u64, - pub channel_id: u32, -} - -impl Message for PlayMsg {} +#[derive(Debug)] +struct BinaryOperands { lhs: i64, rhs: i64 } +impl Message for BinaryOperands {} ``` -A component can then declare that message type in its `#[component(...)]` impl: - -```rust ignore -use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; - -#[derive(Debug, Clone, Instruction)] -pub enum WaveInst { - SetAmplitude(f64), - Play, -} - -#[derive(Debug, Clone, Message)] -pub struct PlayMsg { - pub when_ns: u64, - pub channel_id: u32, -} - -#[derive(Debug, Clone)] -pub struct ChannelSample { - pub when_ns: u64, - pub channel_id: u32, - pub value: f64, -} +The component declares the message through its `Execute` implementation; +the composite resolves it through one of the route clauses. -#[derive(Debug, Default)] -pub struct WaveGenerator { - amplitude: f64, -} +## The three message sources -#[component(instruction = WaveInst, message = PlayMsg, effect = ChannelSample)] -impl WaveGenerator { - fn execute(&mut self, inst: WaveInst, msg: PlayMsg) -> Result> { - match inst { - WaveInst::SetAmplitude(v) => { - self.amplitude = v; - Ok(Effects::none()) - } - WaveInst::Play => Ok(Effects::one(ChannelSample { - when_ns: msg.when_ns, - channel_id: msg.channel_id, - value: self.amplitude, - })), - } - } -} +```text +message none; // passes NoMessage +message from operand_stack; // calls Supply +message with resolve_message; // calls a composite method ``` -The important thing is that `WaveGenerator` does not decide `when_ns` or `channel_id`. -It just consumes the already-resolved `PlayMsg`. - -## Why The Composite Owns Message Resolution - -The composite runtime is the right place to build messages because it owns the broader execution context. - -That often includes: - -- cross-component state -- scheduler or runtime state -- stacks, clocks, frames, or device metadata -- validation and access control - -The component should not have to reconstruct that context on its own. - -So the execution flow usually looks like this: - -1. the composite receives or dispatches an instruction -2. the composite inspects runtime state and the instruction -3. the composite builds the message -4. the composite executes the component with `(instruction, message)` - -That is the mental model to keep throughout the rest of this guide. - -## A Small Composite-Author Example - -`#[composite]` generates the device wiring (the outer instruction enum and device metadata), but message resolution is plain Rust that you write next to the composite: build the message from runtime context, then hand `(instruction, message)` to the component via the generated `execute_generated` method. +`message from field` is useful when a reusable component already knows how to +produce the message. `message with method` is the right boundary when several +fields or machine policy must be combined: ```rust ignore -use eyre::Result; -use vihaco::{Effects, GeneratedComponent, Instruction, Message, component, composite}; - -#[derive(Debug, Clone, Instruction)] -enum DeviceInst { - Pulse, -} - -#[derive(Message)] -struct DeviceMsg(&'static str); - -#[derive(Default)] -struct Device { - seen: Vec<&'static str>, -} - -#[component(instruction = DeviceInst, message = DeviceMsg)] -impl Device { - fn execute(&mut self, inst: DeviceInst, msg: DeviceMsg) -> Result> { - match inst { - DeviceInst::Pulse => { - self.seen.push(msg.0); - Ok(Effects::none()) - } - } - } -} - -#[composite] -#[derive(Default)] -struct Pilot { - #[device(0x02, alias = "pulse")] - device: Device, -} - -impl Pilot { - // The composite owns message resolution, then executes the component. - fn step(&mut self, inst: DeviceInst) -> Result> { - let msg = self.resolve_device(&inst)?; - self.device.execute_generated(inst, msg) - } - - fn resolve_device(&mut self, _inst: &DeviceInst) -> Result { - Ok(DeviceMsg("resolved")) +impl Calculator { + fn resolve_add( + &mut self, + _instruction: &calculator::instruction::Add, + ) -> eyre::Result { + Ok(BinaryOperands { lhs: 1, rhs: 2 }) } } ``` -This is the core composite-author contract: - -- the component says which message type it needs -- the composite provides a resolver for that instruction family -- the resolver returns the message value the component will consume - -In other words, the component defines the input shape, but the composite decides the actual input value for that step. - -## A Richer Example: Resolving A Signal Message - -A real runtime shows a richer version of the same idea. A signal-generator component expects a `SignalMessage`: - -```rust ignore -use vihaco::{Effects, Message, component}; - -#[derive(Debug, Clone, Copy, PartialEq, Message)] -pub enum SignalMessage { - None, - Poly4([f64; 4]), - Duration(u64), -} - -#[component(instruction = SignalInst, message = SignalMessage)] -impl SignalGenerator { - fn execute(&mut self, inst: SignalInst, msg: SignalMessage) -> eyre::Result> { - // component consumes a resolved message here - let _ = (inst, msg); - Ok(Effects::none()) - } -} -``` - -But the component does not know how to create `Poly4([f64; 4])` or `Duration(u64)` on its own. -That comes from composite-owned runtime state. The composite resolves the message first -(the exact accessors — a stack, a clock — depend on your runtime; the shape is what matters): - -```rust ignore -fn resolve_signal(&mut self, inst: &SignalInst) -> eyre::Result { - match inst { - SignalInst::Poly(_addr) => { - let p3: f64 = self.cpu.stack_pop()?.try_into()?; - let p2: f64 = self.cpu.stack_pop()?.try_into()?; - let p1: f64 = self.cpu.stack_pop()?.try_into()?; - let p0: f64 = self.cpu.stack_pop()?.try_into()?; - Ok(SignalMessage::Poly4([p0, p1, p2, p3])) - } - SignalInst::Play if self.signal.is_idle() => { - let cycles: u64 = self.cpu.stack_pop()?.try_into()?; - let duration_ns = cycles - .checked_mul(self.clock.resolution_ns()) - .ok_or_else(|| eyre::eyre!("play duration overflow"))?; - Ok(SignalMessage::Duration(duration_ns)) - } - SignalInst::Play => Ok(SignalMessage::None), - } -} -``` - -Then the runtime executes the component with that resolved value: - -```rust ignore -use vihaco::GeneratedComponent; - -let msg = self.resolve_signal(&signal_inst)?; -let effects = self.signal.execute_generated(signal_inst, msg)?; -assert_eq!(effects, Effects::one(())); -``` - -This example shows why composites own message resolution: - -- the message depends on host stack state -- the message depends on clock resolution -- the message depends on whether the generator is idle -- the component can stay focused on execution once the message is ready - -When a component returns a non-unit effect, hand-written runtimes normally either: - -- extract exactly one control/data effect with `expect_exactly_one_effect(...)`, or -- lift the returned values into a runtime-local sum-effect enum and continue that effect set explicitly - -## When To Use `message = ()` - -Use `message = ()` when the component can execute directly from: - -- the instruction itself -- the component's own local state - -For example: - -```rust ignore -use eyre::Result; -use vihaco::{Effects, Instruction, component}; - -#[derive(Debug, Clone, Instruction)] -pub enum LampInst { - On, - Off, -} - -#[derive(Debug, Default)] -pub struct Lamp { - on: bool, -} - -#[component(instruction = LampInst, message = ())] -impl Lamp { - fn execute(&mut self, inst: LampInst, _msg: ()) -> Result> { - self.on = matches!(inst, LampInst::On); - Ok(Effects::none()) - } -} -``` - -In this case there is nothing meaningful for the composite to resolve, so a unit message is the right fit. - -## Composite Message Types - -When an outer composite wraps inner components, it can also wrap their message types. - -```rust -use vihaco::Message; - -struct DemoMsg; -impl Message for DemoMsg {} - -enum CompositeMsg { - Inner(DemoMsg), -} -impl Message for CompositeMsg {} -``` - -This pattern keeps the outer component or composite boundary explicit: - -- outer instructions wrap inner instructions -- outer messages wrap inner messages -- routing stays visible in the outer type signatures - -The same design rule applies here too: the outer composite layer decides which inner message variant to construct. - -## `Instruction` Vs `Message` Vs `Effect` - -A simple way to choose the right type is: - -- use `Instruction` for bytecode-visible operations -- use `Message` for resolved execution input produced by the composite -- use `Effect` for returned values consumed after execution - -Good candidates for `Message`: - -- timing data derived from a runtime clock -- values popped from a stack before execution -- validated handles or resolved addresses -- execution-local context that should not be part of source syntax - -Usually not a good fit for `Message`: - -- the main operation being requested -- long-lived component state -- broadcast or runtime follow-up values that belong in the effect stream - -## Practical Guidance +The resolver returns an owned value. That matters for parked operations: the +component must not retain a borrow into the composite while waiting for a +completion. -- Start with `message = ()` unless execution genuinely needs resolved input. -- If a component needs context from the wider runtime, prefer resolving that context into a message. -- Keep message types plain and specific to execution needs. -- Let composites do lookups, stack access, timing derivation, and validation before calling component execution. -- Keep effects separate from messages so post-execution output stays explicit. +## Message, instruction, and effect -## What Comes Next +- The instruction is the runtime operation selected by a composite route. +- The message is resolved input for this execution attempt. +- The effect is owned output returned in `StepResult`. -Messages make the most sense alongside the surrounding component and composite model. +Use instruction fields for values that are part of the encoded/runtime +operation. Use messages for values supplied by the current machine state. +Use effects for state changes or events that the parent must observe or route. -Continue with: +`message = ...` on the old component attribute is not part of the current +API. The message contract belongs to `Execute`, and its source belongs to +the composite route. -- [Building Components With `vihaco`](/guide/components) -- [Observing Effects With `#[observe]`](/guide/observers) -- [Defining A Composite With `vihaco`](/guide/composites) +Continue with [Defining a Composite](/guide/composites). diff --git a/docs/src/pages/guide/observers.md b/docs/src/pages/guide/observers.md index 6f71e538..85cbcc49 100644 --- a/docs/src/pages/guide/observers.md +++ b/docs/src/pages/guide/observers.md @@ -2,500 +2,62 @@ layout: ../../layouts/Guide.astro title: Observing Effects slug: observers -description: "How #[observe] works — declaring Observe impls on standalone observers and on components that also react to delivered effects." +description: "Borrow effects for tracing and diagnostics while a composite routes ownership to one handler." --- -# Observing Effects With `#[observe]` +# Observing Effects With `Observe` -`vihaco` separates execution from effect delivery: - -- components execute instructions and return effects -- `#[observe]` lets any type react to delivered effect types -- a runtime wires effect delivery together - -This guide explains what `#[observe]` is for and how to use it, both on standalone observer types and on components alike. - -## What `#[observe]` Looks Like - -An explicit `impl Observe` declares which delivered effect types a type handles. +Observers inspect an effect without consuming it. Implement +`Observe` for a field that records traces, updates metrics, or performs +diagnostics. `R` is the composite route marker, so the same effect type can be +observed differently on different routes. ```rust use eyre::Result; use vihaco::{Effects, Observe}; -#[derive(Debug, Clone)] -pub struct StdoutEffect(pub String); - -#[derive(Debug, Default)] -pub struct StdoutCollector { - lines: Vec, -} - -impl Observe for StdoutCollector { - type Effect = (); - type Error = eyre::Report; - - fn observe(&mut self, effect: &StdoutEffect) -> Result> { - self.lines.push(effect.0.clone()); - Ok(Effects::none()) - } -} -``` - -For a plain observer, the handler method: - -- takes `&mut self` -- takes `&EffectType` -- returns `Result, Error>` -- must be named `observe_` such as `observe_stdout_effect` - -The macro generates an `Observe` trait impl that delegates to the handler method. - -Observer handlers can also synthesize follow-up effects. If a handler returns values instead of `Effects::none()`, the runtime continues them in declared depth-first order. - -### Multiple Handlers Per Effect - -You can define multiple handler methods for the same effect type by adding a suffix after the base name: - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, observe}; -# #[derive(Debug, Clone)] -# pub struct ChannelFrame; -# #[derive(Debug, Default)] -# pub struct Oscilloscope { samples: Vec } -#[observe(ChannelFrame, effect = ())] -impl Oscilloscope { - fn observe_channel_frame_capture(&mut self, effect: &ChannelFrame) -> Result> { - self.samples.push(effect.clone()); - Ok(Effects::none()) - } - - fn observe_channel_frame_log(&mut self, effect: &ChannelFrame) -> Result> { - println!("frame received: {:?}", effect); - Ok(Effects::none()) - } -} -``` - -All methods matching `observe_` or `observe__*` are called when the effect is delivered. - -### Multiple Effect Types - -A single `#[observe]` block can handle multiple delivered effect types: - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, observe}; -# #[derive(Debug, Clone)] -# pub struct StdoutEffect(pub String); -# #[derive(Debug, Clone)] -# pub struct ChannelSample { pub value: f64 } -# #[derive(Debug, Default)] -# pub struct MultiObserver; -#[observe(StdoutEffect, ChannelSample, effect = ())] -impl MultiObserver { - fn observe_stdout_effect(&mut self, effect: &StdoutEffect) -> Result> { - let _ = effect; - Ok(Effects::none()) - } - - fn observe_channel_sample(&mut self, effect: &ChannelSample) -> Result> { - let _ = effect; - Ok(Effects::none()) - } -} -``` - -The macro generates a separate `Observe` impl for each listed effect type. - -## When To Declare `effect = ...` - -An `#[observe(...)]` block defaults to a `()` follow-up effect type. Declare an explicit follow-up type with `effect = ...` once the boundary does typed continuation work instead of a simple `Effects<()>` handoff. In practice, write `effect = CompositeEffect` on the `#[observe(...)]` block when any of these are true: - -- the same `#[observe(...)]` block handles multiple delivered effect types -- the delivered effect has multiple matching handler methods -- any handler returns typed follow-up effects instead of `Effects<()>` - -That keeps continuation explicit and allows each child observer to return its own local follow-up type as long as it converts into the composite effect with `Into`. - -```rust -use eyre::Result; -use vihaco::{Effects, Observe}; - -#[derive(Debug, Clone)] -pub struct ChannelFrame; - -#[derive(Debug, Clone)] -pub struct FrameRendered; - -#[derive(Debug, Clone)] -pub enum RuntimeEffect { - Rendered(FrameRendered), -} - -impl From for RuntimeEffect { - fn from(value: FrameRendered) -> Self { - Self::Rendered(value) - } -} +#[derive(Debug)] +struct Line(String); #[derive(Default)] -pub struct Display; - -impl Observe for Display { - type Effect = FrameRendered; - type Error = eyre::Report; - - fn observe(&mut self, effect: &ChannelFrame) -> Result> { - let _ = effect; - Ok(Effects::one(FrameRendered)) - } -} - -#[derive(Default)] -pub struct Runtime { - display: Display, -} - -impl Observe for Runtime { - type Effect = RuntimeEffect; - type Error = eyre::Report; - - fn observe(&mut self, effect: &ChannelFrame) -> Result> { - Ok(Observe::::observe(&mut self.display, effect)?.map(Into::into)) - } -} -``` - -## The Observe Trait - -`Observe` is effect-only: - -```rust ignore -pub trait Observe { - type Effect: 'static; - type Error; - - fn observe(&mut self, effect: &E) -> Result, Self::Error>; -} -``` - -Observers receive only the delivered effect. If an observer needs extra data, use one of these two patterns: - -- put the needed data into a staged follow-up effect -- store the needed state inside the observing component and update it through earlier effects - -## Standalone Observers - -The simplest use of `#[observe]` is on a type that only reacts to delivered effects with no instructions or messages of its own: - -```rust -# use eyre::Result; -# use vihaco::{Effects, Observe}; -# #[derive(Debug, Clone)] -# pub struct StdoutEffect(pub String); -#[derive(Debug, Default)] -pub struct StdoutCollector { - lines: Vec, -} - -impl Observe for StdoutCollector { - type Effect = (); - type Error = eyre::Report; - - fn observe(&mut self, effect: &StdoutEffect) -> Result> { - self.lines.push(effect.0.clone()); - Ok(Effects::none()) - } -} -``` - -A composite owns such an observer as an ordinary field. The `#[observe]` derive gives the field type an `Observe` impl; the runtime delivers effects to it by calling that impl (see [Wire It Together](#wire-it-together) below): - -```rust ignore -use vihaco::composite; - -#[composite] -#[derive(Debug, Default)] -pub struct WaveComposite { - #[device(0x00, alias = "wave")] - wave: WaveGenerator, - - // A plain field; the runtime delivers StdoutEffect to it explicitly. - stdout: StdoutCollector, -} -``` - -`#[composite]` is transitional scaffolding for the generated device wiring (the outer instruction enum and the device metadata). The underlying model is still ordinary component execution plus typed effect observation, continued by hand-written runtime code. - -## Components That Also Observe - -`#[observe]` is not limited to standalone observer types. A component that executes instructions can also observe delivered effects. - -The important design shift is that the observer sees the effect directly. If it needs post-processed data, an earlier step should emit a richer staged effect rather than relying on borrowed context. - -```rust ignore -use eyre::Result; -use vihaco::{Effects, component, observe}; - -pub struct ChannelFrame { - pub channel: u32, - pub value: f64, -} - -pub struct FrameRendered { - pub frame: ChannelFrame, - pub markers: Vec<[f64; 2]>, -} - -pub enum DisplayOutcome { - Ready(f64), -} - -#[component(instruction = DisplayInst, message = DisplayMsg, effect = DisplayOutcome)] -impl Display { - fn execute( - &mut self, - inst: DisplayInst, - msg: DisplayMsg, - ) -> Result> { - let _ = (inst, msg); - Ok(Effects::none()) - } -} - -#[observe(FrameRendered)] -impl Display { - fn observe_frame_rendered( - &mut self, - effect: &FrameRendered, - ) -> Result> { - let _ = effect; - Ok(Effects::none()) - } -} -``` - -A runtime can stage that richer effect explicitly: - -1. a `Renderer` observes `ChannelFrame` -2. it updates local render state -3. it emits `FrameRendered { frame, markers }` -4. `Display` observes `FrameRendered` - -That keeps all continuation explicit in the effect types. - -## Delivery Ordering - -Effect delivery is performed by the runtime, and the convention is to follow the composite's field order and the continuation graph. - -For multiple follow-up effects returned as `Effects::Many(...)`, continue them left-to-right and depth-first. That means: - -- the first follow-up effect is fully continued before the second begins -- ordering should usually be expressed through staged effect types -- the types should make the stages visible, regardless of how the wiring is written - -## A Complete Example - -The example below shows the full picture: - -- a component that returns effects -- a standalone observer -- a component that also observes - -### Define The Types - -```rust -use eyre::Result; -use vihaco::{Effects, Instruction, Message, component}; +struct Logger { lines: Vec } -#[derive(Debug, Clone, Instruction)] -pub enum WaveInst { - SetAmplitude(f64), - Play, -} - -#[derive(Debug, Clone)] -pub struct PlayMsg { - pub when_ns: u64, - pub channel_id: u32, -} - -impl Message for PlayMsg {} - -#[derive(Debug, Clone, PartialEq)] -pub struct StdoutEffect(pub String); - -#[derive(Debug, Clone, PartialEq)] -pub struct ChannelSample { - pub when_ns: u64, - pub channel_id: u32, - pub value: f64, -} -``` - -### The Producing Component - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, Instruction, Message, component}; -# #[derive(Debug, Clone, Instruction)] -# pub enum WaveInst { SetAmplitude(f64), Play } -# #[derive(Debug, Clone)] -# pub struct PlayMsg { pub when_ns: u64, pub channel_id: u32 } -# impl Message for PlayMsg {} -# #[derive(Debug, Clone, PartialEq)] -# pub struct ChannelSample { pub when_ns: u64, pub channel_id: u32, pub value: f64 } -#[derive(Debug, Default)] -pub struct WaveGenerator { - amplitude: f64, -} - -#[component(instruction = WaveInst, message = PlayMsg, effect = ChannelSample)] -impl WaveGenerator { - fn execute(&mut self, inst: WaveInst, msg: PlayMsg) -> Result> { - match inst { - WaveInst::SetAmplitude(v) => { - self.amplitude = v; - Ok(Effects::none()) - } - WaveInst::Play => Ok(Effects::one(ChannelSample { - when_ns: msg.when_ns, - channel_id: msg.channel_id, - value: self.amplitude, - })), - } - } -} -``` - -### A Standalone Observer - -```rust -# use eyre::Result; -# use vihaco::{Effects, Observe}; -# #[derive(Debug, Clone)] -# pub struct StdoutEffect(pub String); -#[derive(Debug, Default)] -pub struct StdoutCollector { - lines: Vec, -} - -impl Observe for StdoutCollector { +impl Observe for Logger { type Effect = (); type Error = eyre::Report; - fn observe(&mut self, effect: &StdoutEffect) -> Result> { - self.lines.push(effect.0.clone()); + fn observe(&mut self, line: &Line) -> Result> { + self.lines.push(line.0.clone()); Ok(Effects::none()) } } ``` -### A Component That Also Observes - -```rust ignore -# use eyre::Result; -# use vihaco::{Effects, Instruction, component, observe}; -# #[derive(Debug, Clone, PartialEq)] -# pub struct StdoutEffect(pub String); -# #[derive(Debug, Clone, PartialEq)] -# pub struct ChannelSample { pub when_ns: u64, pub channel_id: u32, pub value: f64 } -#[derive(Debug, Default)] -pub struct Recorder { - samples: Vec, - count: usize, -} +In a `composite!` route, list observers in the order they should run: -#[derive(Debug, Clone, Instruction)] -pub enum RecorderInst { - GetCount, -} - -#[component(instruction = RecorderInst, message = (), effect = StdoutEffect)] -impl Recorder { - fn execute(&mut self, inst: RecorderInst, _msg: ()) -> Result> { - match inst { - RecorderInst::GetCount => Ok(Effects::one(StdoutEffect(format!( - "recorded {} samples", - self.count - )))), - } - } -} - -#[observe(ChannelSample)] -impl Recorder { - fn observe_channel_sample(&mut self, effect: &ChannelSample) -> Result> { - self.samples.push(effect.clone()); - self.count += 1; - Ok(Effects::none()) - } +```text +effects { + observe logger, metrics; + absorb with output_stack; } ``` -### Wire It Together - -`#[composite]` generates the device wiring; the runtime executes a component and then delivers its effects to the matching observers by calling their `Observe` impls. - -```rust ignore -use vihaco::{GeneratedComponent, Observe, composite}; - -#[composite] -#[derive(Debug, Default)] -pub struct WaveComposite { - #[device(0x00, alias = "wave")] - wave: WaveGenerator, - - #[device(0x01, alias = "recorder")] - recorder: Recorder, +Each observer borrows the same effect. The handler then receives ownership +exactly once. This makes the ownership flow clear: - // Plain observer field — delivered to by hand below. - stdout: StdoutCollector, -} - -impl WaveComposite { - fn play(&mut self, msg: PlayMsg) -> eyre::Result<()> { - // 1. WaveGenerator executes Play and returns a ChannelSample. - let samples = self.wave.execute_generated(WaveInst::Play, msg)?; - // 2. Deliver each ChannelSample to the Recorder (which observes it). - for sample in samples { - Observe::::observe(&mut self.recorder, &sample)?; - } - Ok(()) - } - - fn report(&mut self) -> eyre::Result<()> { - // 3. Recorder executes GetCount and returns a StdoutEffect... - let lines = self.recorder.execute_generated(RecorderInst::GetCount, ())?; - // 4. ...which the runtime delivers to the StdoutCollector. - for line in lines { - Observe::::observe(&mut self.stdout, &line)?; - } - Ok(()) - } -} +```text +Execute -> Effects -> Observe(&E) ... -> Handle(E) ``` -The flow: - -1. `WaveGenerator` executes `Play` and returns a `ChannelSample`. -2. The runtime delivers that `ChannelSample` to `Recorder`. -3. `Recorder` updates local state and returns `Effects::none()` from its observer handler. -4. When `Recorder` later executes `GetCount`, it returns a `StdoutEffect`, and the runtime delivers that effect to `StdoutCollector`. - -## Design Guidance - -- Use standalone `#[observe]` when a type only reacts to effects. -- Use `#[observe]` alongside `#[component]` when a device needs to react to effects from other components. -- Make effect types plain standalone Rust types. -- Prefer putting `#[observe]` on the real consumer type, not a forwarding wrapper. -- If a type is conceptually a log sink, recorder, projection, renderer, or simulation consumer with no instructions of its own, model it as a standalone observer. -- Prefer staged follow-up effects over hidden cross-field delivery context. - -## What Comes Next +The observer's associated `Effect` is reserved for typed follow-up work. The +current generated route dispatch does not automatically schedule those +follow-up effects; a future runtime extension may make that continuation +explicit. Until then, return `Effects::none()` or handle follow-up effects in +your own runtime boundary. -After understanding `#[observe]`, the next step is to see how composite wiring ties instruction dispatch and effect continuation together. +An observer is an ordinary component field; it does not need an instruction +catalog or a message source. A component can also implement `Observe` when it +needs to react to another component's output. -Continue with [Defining A Composite With `vihaco`](/guide/composites). +See [Defining a Composite](/guide/composites) for effect handlers and route +selection. diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro index 688e683f..b2c2fde6 100644 --- a/docs/src/pages/index.astro +++ b/docs/src/pages/index.astro @@ -87,7 +87,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");
{observeSrc}

Execution and effect delivery are separate. Anything can - #[observe] an effect type — a log sink, a recorder, or + Observe an effect type — a log sink, a recorder, or another component reacting to what its neighbours produced.

@@ -112,7 +112,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, "");

Components consume a resolved Message and return typed Effects. Observers react to those effects with - #[observe], keeping execution and delivery cleanly + Observe, keeping execution and delivery cleanly separated.

diff --git a/docs/src/pages/quickstart.astro b/docs/src/pages/quickstart.astro index aab4719d..7fc8379d 100644 --- a/docs/src/pages/quickstart.astro +++ b/docs/src/pages/quickstart.astro @@ -40,9 +40,8 @@ const toc = [
vihaco foundation
The framework. The Instruction / Message / - Effects types and their derives, the - #[component], #[observe], and - #[composite] macros, the module / + Effects types, the component! and + composite! macros, the module / syntax / runtime layers, and the Value / Type value model. Re-exports the macros, so most projects depend only on this crate. @@ -53,8 +52,8 @@ const toc = [ cpu::RuntimeInstruction (constants, arithmetic, branches, halt, …), a pattern-derived cpu::SurfaceInstruction, and the - StepOutcome control-flow effect. Use it directly, or as a - reference for writing your own components. + execution component. Use it directly, or as a reference for writing + your own components.
vihaco-parser-derive parser
@@ -74,9 +73,8 @@ const toc = [
vihaco-runtime-derive internal
- The procedural macros behind #[derive(Message)], - #[component], #[observe], and - #[composite] (#[derive(Instruction)] lives in + The procedural macros behind component! and + composite! (#[derive(Instruction)] lives in vihaco-abi-derive). Both are re-exported through vihaco — you rarely depend on them directly.
@@ -117,20 +115,17 @@ eyre = "0.6" # vihaco APIs return eyre::Result

§ 4A first component

- A component bundles three things: an Instruction enum (the - operations), an optional Message (resolved execution input), - and an optional effect type (what execution returns). The - #[component(...)] attribute generates the runtime glue from a - single execute method. + A component declares instruction products with component!. + Each product gets its own Execute<I> implementation, so its + message, effect, and fault types can be specific to that operation.

{quickstartSrc}

- execute_generated is the runtime entry point the macro - generates (via the GeneratedComponent trait); - expect_exactly_one_effect is a helper for the common - single-effect case. For the full data-flow model — when to use a + A composite's generated execute_generated method is the + route-dispatch boundary; components themselves use Execute + directly. For the full data-flow model — when to use a Message, how effects are delivered to observers — read Building Components.

diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8f99aa9c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "vihaco", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "pnpm": "^11.20.0" + } + }, + "node_modules/pnpm": { + "version": "11.20.0", + "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-11.20.0.tgz", + "integrity": "sha512-mm8zCpW2ZEbqCI+vFSFAWooB8H/ecSTMmVjf7VLUu0NnN+ZbCPhfN7Rvy6N1CSVYrFEmK4FoRLIvY0Bu0Wa/7g==", + "license": "MIT", + "bin": { + "pn": "bin/pnpm.mjs", + "pnpm": "bin/pnpm.mjs", + "pnpx": "bin/pnpx.mjs", + "pnx": "bin/pnpx.mjs" + }, + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..19bd4e82 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "pnpm": "^11.20.0" + } +} diff --git a/vision/composite-syntax-runtime-plan.md b/vision/composite-syntax-runtime-plan.md new file mode 100644 index 00000000..666b94a9 --- /dev/null +++ b/vision/composite-syntax-runtime-plan.md @@ -0,0 +1,566 @@ +# Composite Syntax and Runtime Instruction Plan + +## Status + +Design plan for the SST-only instruction pipeline. This document defines how a +composite declares source syntax, lowers parsed instructions into runtime +instructions, and executes those instructions through typed component routes. + +Components provide reusable runtime products and `Execute` implementations. +Composites provide the machine-specific SST vocabulary, lowering policy, route +selection, message resolution, and effect handling. + +## Pipeline + +```text +SST section + -> generated composite surface parser + -> ParsedModule + -> composite syntax-resolver trait + -> Vec + -> program-container module installation + -> program-counter execution + -> runtime route selection + -> message resolution + -> Execute + -> effect observation and handling +``` + +The surface and runtime instruction types are distinct: + +```text +surface instruction + -> source/module resolution + -> runtime instruction +``` + +Parsing never executes instructions. Runtime execution never performs source +resolution. + +## Composite declaration + +An executable composite has three relevant parts: + +1. A `#[program]` field that owns the loaded program and program counter. +2. A `syntax` block that defines the composite's public SST vocabulary. +3. A `runtime` block that defines executable routes. + +Illustrative shape: + +```rust +vihaco::composite! { + pub composite ControlMachine { + error = ControlMachineFault; + + #[program] + pub program: ControlProgram; + + #[device(0x01, alias = "processor")] + pub processor: Processor; + + #[device(0x02, alias = "waveform")] + pub waveform: WaveformDevice; + + #[device(0x03, alias = "logic")] + pub logic: LogicDevice; + + #[device(0x04, alias = "sensor")] + pub sensor: SensorDevice; + + #[device(0x05, alias = "optical")] + pub optical: OpticalDevice; + + pub clock: Clock; + pub stdout: StdoutObserver; + } + + syntax { + #[pattern = "'processor::step $0"] + Step(StepSyntax) => lower_step; + + #[pattern = "'waveform::play $0"] + Play(PlaySyntax) => lower_play; + + #[pattern = "'optical::clear"] + Clear => runtime Clear; + } + + runtime { + Step(processor::instruction::Step) => processor { + message with resolve_step; + effects { + handle with handle_step; + } + } + + Play(waveform::instruction::Play) => waveform { + message with resolve_play; + effects { + observe stdout; + handle with handle_waveform; + } + } + + Clear(optical::instruction::Clear) => optical { + message none; + effects { + handle with handle_optical; + } + } + } +} +``` + +The exact field and route names are user-defined. The important distinction is +that syntax names and runtime route names are allowed to differ. + +## Generated modules + +The composite macro generates namespaced modules rather than placing all +products in the composite's parent namespace: + +```rust +pub mod control_machine { + pub mod syntax { + pub enum Instruction { + Step(StepSyntax), + Play(PlaySyntax), + Clear, + } + + pub trait Resolver { + fn lower_step( + &mut self, + instruction: StepSyntax, + ) -> Result, ControlMachineFault>; + + fn lower_play( + &mut self, + instruction: PlaySyntax, + ) -> Result, ControlMachineFault>; + } + } + + pub mod runtime { + pub enum Instruction { + Step(processor::instruction::Step), + Play(waveform::instruction::Play), + Clear(optical::instruction::Clear), + } + + pub trait MessageResolver { + // Methods are generated for `message with ...` routes. + } + } + + pub mod routes { + // Generated route markers and route-specific implementations. + } +} + +pub use control_machine::syntax::Instruction as SurfaceInstruction; +pub use control_machine::runtime::Instruction as RuntimeInstruction; +pub use control_machine::syntax::Resolver as ControlMachineSyntaxResolver; +pub use control_machine::runtime::MessageResolver as ControlMachineMessageResolver; +``` + +The generated syntax enum implements the parser's surface-instruction marker +and parser interface. The runtime enum is the execution boundary and does not +implement source parsing by default. + +## Syntax declarations + +Composite syntax patterns use complete public spellings. The new pattern +grammar does not require an instruction `head`: + +```rust +syntax { + #[pattern = "'waveform::play $0"] + Play(PlaySyntax) => lower_play; +} +``` + +Instruction tokens accept namespaced identifiers: + +```text +instruction-token = identifier, { "::", identifier } ; +``` + +The composite syntax block establishes the instruction syntax class, so +composite-generated instruction enums do not need an explicit +`#[syntax_class(...)]` attribute. User-defined payload types continue to use +the parser derive and syntax classes appropriate to their role. + +### User-defined payload syntax + +The composite owns the instruction prefix. A payload type owns the grammar of +its operands: + +```rust +#[derive(vihaco_parser_derive::Parse)] +#[syntax_class(value)] +#[pattern = "$duration `,` $mode"] +pub struct PlaySyntax { + pub duration: u64, + pub mode: PlayMode, +} + +vihaco::composite! { + // ... + syntax { + #[pattern = "'waveform::play $0"] + Play(PlaySyntax) => lower_play; + } +} +``` + +`$0` invokes `PlaySyntax::parser()`. This keeps nested operand syntax +composable and prevents the composite macro from becoming a second struct +pattern parser. + +### Direct mappings + +Direct mappings are limited initially to unit instructions: + +```rust +syntax { + #[pattern = "'optical::clear"] + Clear => runtime Clear; +} +``` + +The macro constructs the runtime route directly. Argument-bearing instructions +use named lowerers because procedural macros cannot inspect arbitrary external +runtime product definitions and infer safe conversions. + +### Delegated syntax + +Components do not provide parsers in the initial design. A composite may, +however, explicitly delegate an existing syntax vocabulary in the future or +where a reusable parser type already exists: + +```rust +syntax { + #[delegate(host_vm::Instruction, prefix = "processor")] + Processor(host_vm::Instruction) => runtime Processor; +} +``` + +Delegation imports syntax; it does not make the component's instruction enum +the composite execution boundary. + +## Syntax resolution + +The macro generates a public syntax-resolver trait for named lowerers. The +trait is implemented directly by the composite: + +```rust +impl ControlMachineSyntaxResolver for ControlMachine { + fn lower_play( + &mut self, + instruction: PlaySyntax, + ) -> Result, ControlMachineFault> { + let duration_ns = instruction.duration.try_into()?; + + Ok(vec![RuntimeInstruction::Play( + waveform::instruction::Play { duration_ns }, + )]) + } +} +``` + +Lowerers receive only the parsed syntax value. They access module-resolution +state through `self.program` and may use other composite fields when the +machine explicitly permits it. The program object owns the resolution context; +the composite owns the machine-specific lowering policy. + +Every named lowerer returns an owned sequence: + +```rust +Result, CompositeFault> +``` + +This supports one-to-one lowering, source sugar, and one-to-many expansion. +Module-level resolution assigns final instruction addresses after expansion so +labels and source symbols refer to the runtime program rather than the surface +instruction sequence. + +The generated resolver trait contains only named lowerers. Direct mappings do +not create user methods. + +## Multiple runtime routes + +One surface instruction may select different runtime routes based on source +arguments or resolved module information: + +```rust +syntax { + #[pattern = "'arithmetic::add $0"] + Add(AddSyntax) => lower_add; +} + +runtime { + IntegerAdd(arithmetic::instruction::Add) => integer_stack { + message from integer_stack; + effects { + absorb with integer_stack; + } + } + + AddressAdd(arithmetic::instruction::Add) => address_stack { + message from address_stack; + effects { + absorb with address_stack; + } + } +} +``` + +```rust +impl ControlMachineSyntaxResolver for ControlMachine { + fn lower_add( + &mut self, + instruction: AddSyntax, + ) -> Result, ControlMachineFault> { + let route = match instruction.ty { + AddType::Integer => RuntimeInstruction::IntegerAdd( + arithmetic::instruction::Add, + ), + AddType::Address => RuntimeInstruction::AddressAdd( + arithmetic::instruction::Add, + ), + }; + + Ok(vec![route]) + } +} +``` + +The outer runtime variant carries route identity. It selects the target field, +message resolver, effect policy, fault conversion, and any route-specific +timing or scheduling behavior. The inner runtime product describes the +operation executed by the selected component. + +Runtime route selection based on source/module information happens during +syntax resolution. Decisions based on live machine state remain in runtime +message resolution or component execution. + +## Runtime message resolution + +Message resolution is a separate generated public trait. It runs after a +runtime route has been selected: + +```rust +pub trait ControlMachineMessageResolver { + fn resolve_play( + &mut self, + instruction: &waveform::instruction::Play, + ) -> Result; +} +``` + +The implementation may read both the loaded program and live composite state: + +```rust +impl ControlMachineMessageResolver for ControlMachine { + fn resolve_play( + &mut self, + instruction: &waveform::instruction::Play, + ) -> Result { + let template = self.program.lookup_template(instruction.template)?; + let snapshot = self.sensor.snapshot()?; + + Ok(PlayMessage { + template, + snapshot, + }) + } +} +``` + +The resolver receives the inner runtime product, not the outer route variant. +The generated dispatch already knows which route selected it. + +`message none` and `message from field` do not create user trait methods. +Only `message with resolver_method` creates a required message-resolver method. + +Messages should be owned values. A resolver must not return a borrow into the +program image when the operation may park or otherwise outlive the immediate +dispatch call. + +## Program containers + +`#[program]` is a capability marker, not a concrete type requirement. An author +may provide any program container that owns the program image, PC, module +context, and whatever lookup or resolution state the machine needs: + +```rust +pub struct ControlProgram { + pub module: RuntimeModule, + pub pc: u32, + pub context: ProgramContext, + pub strings: StringTable, +} +``` + +The framework keeps program behavior split across focused traits: + +```rust +ProgramCounter +GetProgramInfo +LoadOwnSstSection +LoadSstSection +InstallProgramModule +``` + +The exact combination is validated by generated call sites. A program type is +not required to expose strings, constants, bytecode, or any other capability it +does not use. + +The installation capability is intentionally small: + +```rust +pub trait InstallProgramModule { + type Instruction; + type Module; + type Context; + + fn install_module( + &mut self, + module: Self::Module, + context: ContextHandle, + ) -> eyre::Result<()>; +} +``` + +Installation replaces the runtime module, context, and PC as one operation. +Program-specific lookup APIs remain author-defined. The framework does not +require a universal `resolve_string` or `resolve_constant` method. + +## SST loading + +The generated root loading path uses the existing multi-section loading model: + +```text +LoadSstSection(root) + -> generated composite LoadOwnSstSection + -> parse root syntax + -> lower through ControlMachineSyntaxResolver + -> build temporary runtime module + -> InstallProgramModule on #[program] + -> forward direct child sections to #[loadable] fields +``` + +The root program is resolved independently of arbitrary live child-device +state. Child sections may provide explicit load metadata through the program's +resolution context, but syntax lowering does not inspect arbitrary device +fields. + +The load is transactional. Parsing, lowering, expansion, label assignment, and +module construction complete before the program container replaces its current +module. A failure leaves the previously loaded program intact. + +Generated composite methods should include: + +```rust +fn load_source(&mut self, source: &str) -> Result<(), ControlMachineFault>; + +fn load_parsed( + &mut self, + parsed: ParsedModule, +) -> Result<(), ControlMachineFault>; +``` + +`load_parsed` is the primary unit-testing boundary for lowering. It avoids +coupling resolver tests to text parsing or section-container construction. + +SST is the only loading format covered by this design. Bytecode loading is +outside the scope of the new pipeline. + +## Errors and diagnostics + +Named syntax lowerers and message resolvers use the composite's declared error +type: + +```rust +Result +``` + +Section-loading traits continue to use `eyre::Result` so they can attach +section, function, and source-location context. Generated loading converts and +enriches composite faults at that boundary. + +Generated resolution should identify the current function and instruction when +propagating a lowerer failure. A new framework-wide structured error type is +not required initially. + +## Effect handling + +Runtime routes continue to define effect observation and handling: + +```rust +runtime { + Play(waveform::instruction::Play) => waveform { + message with resolve_play; + effects { + observe stdout; + handle with handle_waveform; + } + } +} +``` + +The composite macro generates route-specific dispatch. Custom effect handlers +remain ordinary user methods; no generated effect-handler trait is required in +the initial design. + +## Parser changes + +The parser derive and shared parser machinery need the following changes for +the new composite model: + +- Remove the requirement for instruction `head`. +- Support complete namespaced instruction tokens in patterns. +- Keep `syntax_class` for standalone user-defined payload types. +- Allow composite-generated instruction enums to receive parser metadata + without requiring user-written `#[derive(Parse)]` declarations. +- Keep payload parsing compositional through each payload type's `Parse` + implementation. +- Do not require reusable components to provide parsers. + +The parser derive's existing pattern validation remains valuable: field +bindings must be complete, unambiguous, and type-directed. + +## Implementation phases + +1. Audit `ProgramCounter`, `GetProgramInfo`, `LoadOwnSstSection`, + `LoadSstSection`, and the generated multi-section loading paths. +2. Define and implement `InstallProgramModule` with transactional module, + context, and PC installation. +3. Add composite-generated `syntax` and `runtime` modules. +4. Generate surface instruction enums and parser implementations from + composite syntax entries. +5. Remove instruction `head` requirements and add namespaced pattern tokens. +6. Generate public syntax-resolver and message-resolver traits. +7. Generate SST root loading and `load_parsed` paths for composites with + `#[program]` and `syntax`. +8. Generate runtime route dispatch from the `runtime` block. +9. Migrate a small composite with unit direct mappings and argument-bearing + named lowerers. +10. Migrate a composite with one surface instruction selecting multiple + runtime routes. +11. Add transactional-load, interning, one-to-many expansion, source-location, + and message-resolution tests. + +## Non-goals + +This design does not initially provide: + +- component-owned parsers or default component syntax; +- bytecode loading; +- declarative field-by-field runtime constructors; +- a universal program lookup API for strings or constants; +- generated effect-handler traits; +- runtime route selection based on arbitrary live device state during module + resolution; +- automatic compatibility with the old `head`-based parser declarations. From 0c18ff07dbcb36306f201f66de83968ac15a9acb Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Fri, 7 Aug 2026 11:46:30 -0400 Subject: [PATCH 06/15] Allow empty effects block for instructions with no effect; added demo for a hypothetical playable component --- .../design/composite_macro.md | 16 +- crates/vihaco-runtime-derive/src/composite.rs | 22 ++ .../src/composite/codegen.rs | 41 ++-- .../src/composite/syntax.rs | 14 +- crates/vihaco-runtime/src/execute.rs | 31 +++ crates/vihaco-runtime/src/lib.rs | 2 +- .../vihaco-runtime/tests/runtime_contract.rs | 10 +- crates/vihaco/src/lib.rs | 3 +- .../missing-effects-handler.rs | 47 ++++ .../missing-effects-handler.stderr | 13 ++ .../tests/composite_machine_compile_fail.rs | 1 + demos/examples/counter-machine.md | 76 +++++++ demos/examples/counter-machine.rs | 57 +++++ demos/examples/counter-machine/src/machine.rs | 213 ++++++++++++++++++ demos/examples/demo.rs | 2 + demos/examples/demo/stdlib/clock.rs | 28 ++- demos/examples/demo/stdlib/counter.rs | 161 +++++++++++++ demos/examples/demo/stdlib/debug_trace.rs | 41 +++- demos/examples/demo/vihaco/handle.rs | 1 + 19 files changed, 742 insertions(+), 37 deletions(-) create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.stderr create mode 100644 demos/examples/counter-machine.md create mode 100644 demos/examples/counter-machine.rs create mode 100644 demos/examples/counter-machine/src/machine.rs create mode 100644 demos/examples/demo/stdlib/counter.rs diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md index 055d13d7..f8f96b5b 100644 --- a/crates/vihaco-runtime-derive/design/composite_macro.md +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -199,8 +199,8 @@ selected by multiple routes. The payload type is passed unchanged to `Execute`. The macro does not create a component-wide instruction enum or insert implicit conversions. -Each route requires exactly one message clause and one effect handler. It may list zero or more -observers: +Each route requires exactly one message clause. Effect-producing routes include exactly one +effect handler and may list zero or more observers: ```text message none; @@ -213,6 +213,18 @@ effects { } ``` +An instruction that intentionally emits no effects may omit the `effects` block entirely: + +```text +Reset(ResetInstruction) => gate_beam { + message none; +} +``` + +For such a route, the selected component instruction must use `NoEffect` as its `Execute::Effect` +type. The generated route consumes the returned effect stream without invoking observers or a +handler, and the type check prevents another effect type from being silently discarded. + The handler alternatives are exclusive: ```text diff --git a/crates/vihaco-runtime-derive/src/composite.rs b/crates/vihaco-runtime-derive/src/composite.rs index 89aeae08..1eb802cd 100644 --- a/crates/vihaco-runtime-derive/src/composite.rs +++ b/crates/vihaco-runtime-derive/src/composite.rs @@ -74,4 +74,26 @@ mod tests { assert!(declaration.error.is_none()); assert!(declaration.routes.is_empty()); } + + #[test] + fn parses_routes_without_effects_blocks() { + let declaration: CompositeDeclaration = parse_str( + r#" + composite CounterMachine { + error = CounterMachineFault; + counter_group: CounterGroup, + } + runtime_instructions { + Queue(QueueInstruction) => counter_group { + message none; + } + } + "#, + ) + .unwrap(); + + assert_eq!(declaration.routes.len(), 1); + assert!(declaration.routes[0].observers.is_empty()); + assert!(declaration.routes[0].handler.is_none()); + } } diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index ddf65165..adac8bbf 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::{Field, Generics, Ident, Result, Type}; use super::syntax::{CompositeDeclaration, Handler, MessageSource, RouteDeclaration}; @@ -88,13 +88,14 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result>::Effect); let error_type = error.as_ref().expect("validated executable composite"); - let body = match route.handler.as_ref().expect("validated handler") { + let body = match handler { Handler::Absorb(field) => { let absorb_ty = field_ty(field); quote! { @@ -106,7 +107,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result::into) }, }; - quote! { + Some(quote! { impl #impl_generics #root::Handle<#effect, #route_module::#marker> for #name #ty_generics #where_clause { @@ -116,7 +117,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result Result::into)?; } }); + let effect_handling = if route.handler.is_some() { + quote! { + for effect in result.effects { + #( #observers )* + >::Effect, + #route_module::#marker + >>::handle(self, effect) + .map_err(::std::convert::Into::<#error_type>::into)?; + } + } + } else { + let no_effect_assertion = quote_spanned! {route.variant.span()=> + let _: #root::NoEffect = effect; + }; + quote! { + for effect in result.effects { + #no_effect_assertion + } + } + }; quote! { #instruction_ident::#variant(instruction) => { let message = #message; @@ -161,14 +183,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result::into)?; - for effect in result.effects { - #( #observers )* - >::Effect, - #route_module::#marker - >>::handle(self, effect) - .map_err(::std::convert::Into::<#error_type>::into)?; - } + #effect_handling Ok(result.execution) } } diff --git a/crates/vihaco-runtime-derive/src/composite/syntax.rs b/crates/vihaco-runtime-derive/src/composite/syntax.rs index 05e7a672..fe327592 100644 --- a/crates/vihaco-runtime-derive/src/composite/syntax.rs +++ b/crates/vihaco-runtime-derive/src/composite/syntax.rs @@ -142,7 +142,6 @@ impl Parse for RouteDeclaration { let mut message_source = None; let mut observers = Vec::new(); let mut handler = None; - let mut saw_effects = false; while !body.is_empty() { if body.peek(message) { @@ -166,12 +165,11 @@ impl Parse for RouteDeclaration { message_source = Some(source); } else if body.peek(effects) { body.parse::()?; - if saw_effects { - return Err(body.error("route has more than one effects block")); - } - saw_effects = true; let effects_body; syn::braced!(effects_body in body); + if handler.is_some() || !observers.is_empty() { + return Err(body.error("route has more than one effects block")); + } parse_effects(&effects_body, &mut observers, &mut handler)?; } else { return Err(body.error("expected a message clause or effects block")); @@ -180,12 +178,6 @@ impl Parse for RouteDeclaration { let message = message_source .ok_or_else(|| syn::Error::new(variant.span(), "route is missing a message clause"))?; - if !saw_effects { - return Err(syn::Error::new( - variant.span(), - "route is missing an effects block", - )); - } Ok(Self { variant, diff --git a/crates/vihaco-runtime/src/execute.rs b/crates/vihaco-runtime/src/execute.rs index 82d499fa..632e728d 100644 --- a/crates/vihaco-runtime/src/execute.rs +++ b/crates/vihaco-runtime/src/execute.rs @@ -8,6 +8,13 @@ use crate::Effects; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct NoMessage; +/// Marker effect for instructions that intentionally emit no effects. +/// +/// This is distinct from [`std::convert::Infallible`], which describes an operation that cannot +/// fail. `NoEffect` describes the effect channel of an instruction instead. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NoEffect {} + /// Outcome of one instruction step. /// /// This is independent of any timing model. It answers whether the parent @@ -40,3 +47,27 @@ pub trait Execute { message: Self::Message, ) -> Result, Self::Fault>; } + +#[macro_export] +macro_rules! complete { + () => { + Ok(StepResult { + effects: vihaco::Effects::None, + execution: vihaco::Execution::Complete, + }) + }; + + ($effect:expr) => { + Ok(StepResult { + effects: vihaco::Effects::One($effect), + execution: vihaco::Execution::Complete, + }) + }; + + ($($effects:expr),+) => { + Ok(StepResult { + effects: vihaco::Effects::Many($effects), + execution: vihaco::Execution::Complete, + }) + }; +} diff --git a/crates/vihaco-runtime/src/lib.rs b/crates/vihaco-runtime/src/lib.rs index 6b488d8d..ec24cfe7 100644 --- a/crates/vihaco-runtime/src/lib.rs +++ b/crates/vihaco-runtime/src/lib.rs @@ -21,7 +21,7 @@ pub use vihaco_abi::{Effects, metadata}; pub use vihaco_bytecode::{BytecodeSectionView, SstSectionView}; pub use vihaco_module::loader; -pub use execute::{Execute, Execution, NoMessage, StepResult}; +pub use execute::{Execute, Execution, NoEffect, NoMessage, StepResult}; pub use generated::{CompositeMetadata, expect_exactly_one_effect}; pub use handle::{Absorb, Handle}; pub use marker::Message; diff --git a/crates/vihaco-runtime/tests/runtime_contract.rs b/crates/vihaco-runtime/tests/runtime_contract.rs index 1bfa8295..dabf122a 100644 --- a/crates/vihaco-runtime/tests/runtime_contract.rs +++ b/crates/vihaco-runtime/tests/runtime_contract.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT use vihaco_runtime::{ - Absorb, Effects, Execute, Execution, Handle, NoMessage, Observe, StepResult, Supply, + Absorb, Effects, Execute, Execution, Handle, NoEffect, NoMessage, Observe, StepResult, Supply, }; #[derive(Debug, PartialEq, Eq)] @@ -110,3 +110,11 @@ fn supply_absorb_observe_and_handle_are_route_capabilities() { fn execution_has_complete_and_parked_states() { assert_ne!(Execution::Complete, Execution::Parked); } + +#[test] +fn no_effect_is_a_distinct_uninhabited_effect_type() { + fn accepts_no_effect(_: NoEffect) {} + + let _: fn(NoEffect) = accepts_no_effect; + assert_eq!(std::mem::size_of::(), 0); +} diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index d4cf066c..1d121b46 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -42,7 +42,8 @@ pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; pub use runtime::{ Absorb, CompositeMetadata, EffectSink, Execute, Execution, Handle, Message, - Message as MessageMarker, NoMessage, Observe, StepResult, Supply, expect_exactly_one_effect, + Message as MessageMarker, NoEffect, NoMessage, Observe, StepResult, Supply, complete, + expect_exactly_one_effect, }; pub use traits::{FromBytes, FromText, GetProgramInfo, Reset}; pub use vihaco_parser::SurfaceInstruction; diff --git a/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs new file mode 100644 index 00000000..da2a0a7d --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::convert::Infallible; + +use vihaco::{Execute, NoMessage, StepResult}; + +struct Target; +#[derive(Clone)] +struct Instruction; +struct Effect; +enum Fault {} + +impl Execute for Target { + type Message = NoMessage; + type Effect = Effect; + type Fault = Infallible; + + fn execute( + &mut self, + _instruction: &Instruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + unreachable!() + } +} + +impl From for Fault { + fn from(fault: Infallible) -> Self { + match fault {} + } +} + +vihaco::composite! { + pub composite Machine { + error = Fault; + target: Target, + } + + runtime_instructions { + Queue(Instruction) => target { + message none; + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.stderr b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.stderr new file mode 100644 index 00000000..524d2235 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.stderr @@ -0,0 +1,13 @@ +error[E0308]: mismatched types + --> tests/compile_fail/composite_machine/missing-effects-handler.rs:41:9 + | +34 | / vihaco::composite! { +35 | | pub composite Machine { +36 | | error = Fault; +37 | | target: Target, +... | +41 | | Queue(Instruction) => target { + | | ^^^^^ expected `NoEffect`, found `Effect` +... | +45 | | } + | |_- expected due to this diff --git a/crates/vihaco/tests/composite_machine_compile_fail.rs b/crates/vihaco/tests/composite_machine_compile_fail.rs index dc68281b..5b6ca4eb 100644 --- a/crates/vihaco/tests/composite_machine_compile_fail.rs +++ b/crates/vihaco/tests/composite_machine_compile_fail.rs @@ -8,4 +8,5 @@ fn composite_machine_rejects_ambiguous_wiring() { t.compile_fail("tests/compile_fail/composite_machine/duplicate-loadable-name.rs"); t.compile_fail("tests/compile_fail/composite_machine/invalid-loadable-name.rs"); t.compile_fail("tests/compile_fail/composite_machine/loadable-without-device.rs"); + t.compile_fail("tests/compile_fail/composite_machine/missing-effects-handler.rs"); } diff --git a/demos/examples/counter-machine.md b/demos/examples/counter-machine.md new file mode 100644 index 00000000..cd31ba42 --- /dev/null +++ b/demos/examples/counter-machine.md @@ -0,0 +1,76 @@ +# Counter machine + +The counter-machine example demonstrates a clock-driven composite that plays +multiple channels concurrently. `CounterGroup` is the channel manager: + +- `Queue { start, duration }` adds a counter to the pending queue. +- `Play` moves all pending counters into the active set. +- Each active counter advances once per global clock tick. +- A `PlayReport` describes what every active counter emitted on that tick. + +The machine itself does not own counter state. It owns the `GlobalClock`, the +runtime program counter, and the event loop that decides when the group is +sampled. This is the same boundary intended for a future waveform or FPGA +component: the component evaluates its channels, while the machine supplies +the timeline. + +## Execution flow + +The example program queues two counters and then starts playback: + +```text +Queue(start = 10, duration = 2) +Queue(start = 100, duration = 4) +Play +``` + +The `Play` instruction starts both counters at the same global time. The +machine schedules the first `AdvanceCounters` event one tick later. Each +advancement produces one report containing all channels that are still active: + +```text +tick 1: Counter 0 -> Advanced(11), Counter 1 -> Advanced(101) +tick 2: Counter 0 -> Done(12), Counter 1 -> Advanced(102) +tick 3: Counter 1 -> Advanced(103) +tick 4: Counter 1 -> Done(104) +``` + +Playback ends when there are no active counters. Counters with a zero duration +are discarded when `Play` starts. + +## Ownership and event loop + +The responsibilities are intentionally separate: + +```text +CounterMachine +├── GlobalClock +├── runtime program and program counter +└── CounterGroup + ├── queued counters + └── active counters +``` + +`Step` executes one runtime instruction at the current tick. A `Play` step +starts queued channels and schedules `AdvanceCounters` if playback is active. +An `AdvanceCounters` event asks `CounterGroup` for its next `PlayReport`, sends +that report to `DebugTrace` with the current global tick, and schedules the +next tick while any channel remains active. + +The machine guards against duplicate advance events when multiple `Play` +instructions occur at the same time. A later `Play` can add newly queued +channels to an already-running group without interrupting existing channels. + +## Future sample rates + +The current implementation samples once per global tick to keep the example +small. `CounterGroup` does not depend on the clock, so a future component or +timing trait can choose a different sample period. The machine would then use +that period when scheduling the next `AdvanceCounters` event, while the +component would continue to own channel evaluation and report construction. + +Run the example with: + +```bash +cargo run -p vihaco-demos --example counter-machine +``` diff --git a/demos/examples/counter-machine.rs b/demos/examples/counter-machine.rs new file mode 100644 index 00000000..fe0a8503 --- /dev/null +++ b/demos/examples/counter-machine.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +//! Clocked counter-group machine. +//! +//! Two counters are queued and played together. Their reports are recorded with the global tick +//! at which each channel was sampled. + +#![allow(dead_code)] + +// The included demo components refer to the facade's `Effects` type through this module root. +use vihaco::Effects; + +#[path = "demo/stdlib/clock.rs"] +mod clock; +#[path = "demo/stdlib/counter.rs"] +mod counter; +#[path = "demo/stdlib/debug_trace.rs"] +mod debug_trace; +#[path = "demo/vihaco/handle.rs"] +mod handle; +#[path = "counter-machine/src/machine.rs"] +mod machine; + +fn main() { + use counter::counter_group::instruction; + use machine::{CounterMachine, CounterMachineInstruction}; + + let program = vec![ + CounterMachineInstruction::Queue(instruction::Queue { + start: 10, + duration: 2, + }), + CounterMachineInstruction::Queue(instruction::Queue { + start: 100, + duration: 4, + }), + CounterMachineInstruction::Play(instruction::Play), + CounterMachineInstruction::Queue(instruction::Queue { + start: 50, + duration: 5, + }), + CounterMachineInstruction::Play(instruction::Play), + CounterMachineInstruction::Queue(instruction::Queue { + start: 50, + duration: 5, + }), + CounterMachineInstruction::Play(instruction::Play), + ]; + + let mut machine = CounterMachine::new(program); + machine.run().expect("counter machine should complete"); + + for record in &machine.debug.records { + println!("{record:?}"); + } +} diff --git a/demos/examples/counter-machine/src/machine.rs b/demos/examples/counter-machine/src/machine.rs new file mode 100644 index 00000000..42067da7 --- /dev/null +++ b/demos/examples/counter-machine/src/machine.rs @@ -0,0 +1,213 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +//! A global-clock coordinator for waveform-like channel components. +//! +//! `CounterGroup` owns queued and active channels. `CounterMachine` owns only the timeline: +//! runtime instructions queue channels or begin playback, and each playback tick asks the group +//! for one `PlayReport`. The report is then sent to observers, which is the same boundary a future +//! FPGA waveform component can use. Sampling is deliberately one global tick for now; a later +//! component timing trait can replace `schedule_next_advance` without moving clock ownership into +//! the channel component. + +use std::convert::Infallible; + +use crate::{ + clock::{ClockFault, GlobalClock, GlobalDuration, GlobalTick}, + counter::counter_group::{self, CounterGroup}, + debug_trace::DebugTrace, +}; +#[derive(Debug, Clone, Copy)] +pub enum MachineEvent { + Step, + AdvanceCounters, +} + +vihaco::composite! { + pub composite CounterMachine { + error = CounterMachineFault; + + pub clock: GlobalClock, + pub counter_group: CounterGroup, + pub debug: DebugTrace, + + program: Vec, + pc: usize, + advance_scheduled: bool, + } + + runtime_instructions { + Queue(counter_group::instruction::Queue) => counter_group { + message none; + } + Play(counter_group::instruction::Play) => counter_group { + message none; + effects { + absorb with debug; + } + } + } +} + +impl CounterMachine { + pub fn new(program: Vec) -> Self { + Self { + clock: GlobalClock::new(), + counter_group: CounterGroup::new(), + debug: DebugTrace::new(), + program, + pc: 0, + advance_scheduled: false, + } + } + + pub fn run(&mut self) -> Result { + self.clock + .schedule_at(GlobalTick::ZERO, MachineEvent::Step)?; + + while let Some((tick, event)) = self.clock.pop_earliest() { + match event { + MachineEvent::Step => self.step(tick)?, + MachineEvent::AdvanceCounters => self.advance_counters(tick)?, + } + } + + Ok(RunOutcome::Completed) + } + + fn step(&mut self, tick: GlobalTick) -> Result<(), CounterMachineFault> { + let Some(instruction) = self.program.get(self.pc).cloned() else { + return Ok(()); + }; + + self.execute_generated(&instruction)?; + self.pc += 1; + + if matches!(instruction, CounterMachineInstruction::Play(_)) + && self.counter_group.is_playing() + && !self.advance_scheduled + { + self.schedule_next_advance(tick)?; + } + + if self.pc < self.program.len() { + let next_step = if matches!(instruction, CounterMachineInstruction::Play(_)) { + tick.checked_add(GlobalDuration(1))? + } else { + tick + }; + self.clock.schedule_at(next_step, MachineEvent::Step)?; + } + Ok(()) + } + + fn advance_counters(&mut self, tick: GlobalTick) -> Result<(), CounterMachineFault> { + self.advance_scheduled = false; + let report = self.counter_group.advance(); + self.debug.record_at(tick.0, &report); + + if self.counter_group.is_playing() { + self.schedule_next_advance(tick)?; + } + Ok(()) + } + + fn schedule_next_advance(&mut self, tick: GlobalTick) -> Result<(), CounterMachineFault> { + self.clock.schedule_at( + tick.checked_add(GlobalDuration(1))?, + MachineEvent::AdvanceCounters, + )?; + self.advance_scheduled = true; + Ok(()) + } + + /// The initial clock position is exposed here only as a convenient scaffold anchor. + pub fn now(&self) -> GlobalTick { + self.clock.now() + } +} + +impl Default for CounterMachine { + fn default() -> Self { + Self::new(vec![]) + } +} + +#[derive(Debug)] +pub enum CounterMachineFault { + Clock(ClockFault), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum RunOutcome { + Completed, +} + +#[cfg(test)] +mod tests { + use super::{CounterMachine, CounterMachineInstruction, RunOutcome}; + use crate::counter::counter_group::instruction; + + #[test] + fn queued_counters_are_advanced_on_shared_global_ticks() { + let program = vec![ + CounterMachineInstruction::Queue(instruction::Queue { + start: 10, + duration: 2, + }), + CounterMachineInstruction::Queue(instruction::Queue { + start: 20, + duration: 3, + }), + CounterMachineInstruction::Play(instruction::Play), + ]; + let mut machine = CounterMachine::new(program); + + assert_eq!(machine.run().unwrap(), RunOutcome::Completed); + assert_eq!(machine.now().0, 3); + assert_eq!(machine.debug.records.len(), 4); + } + + #[test] + fn a_later_play_starts_after_the_first_play_tick() { + let program = vec![ + CounterMachineInstruction::Queue(instruction::Queue { + start: 10, + duration: 2, + }), + CounterMachineInstruction::Play(instruction::Play), + CounterMachineInstruction::Queue(instruction::Queue { + start: 100, + duration: 2, + }), + CounterMachineInstruction::Play(instruction::Play), + ]; + let mut machine = CounterMachine::new(program); + + machine.run().unwrap(); + + let reports = machine + .debug + .records + .iter() + .filter(|record| record.route == "clock") + .map(|record| record.effect.clone()) + .collect::>(); + assert!(reports[0].contains("tick 1")); + assert!(reports[0].contains("CounterId(0)")); + assert!(!reports[0].contains("CounterId(1)")); + assert!(reports[1].contains("CounterId(1)")); + } +} + +impl From for CounterMachineFault { + fn from(fault: ClockFault) -> Self { + CounterMachineFault::Clock(fault) + } +} + +impl From for CounterMachineFault { + fn from(never: Infallible) -> Self { + match never {} + } +} diff --git a/demos/examples/demo.rs b/demos/examples/demo.rs index 84159581..f4179b6b 100644 --- a/demos/examples/demo.rs +++ b/demos/examples/demo.rs @@ -18,6 +18,8 @@ mod arithmetic; mod channel; #[path = "demo/stdlib/clock.rs"] mod clock; +#[path = "demo/stdlib/counter.rs"] +mod counter; #[path = "demo/src/cpu.rs"] mod cpu; #[path = "demo/stdlib/debug_trace.rs"] diff --git a/demos/examples/demo/stdlib/clock.rs b/demos/examples/demo/stdlib/clock.rs index 5af15c25..5a1f7a3b 100644 --- a/demos/examples/demo/stdlib/clock.rs +++ b/demos/examples/demo/stdlib/clock.rs @@ -50,12 +50,38 @@ pub struct Schedule { pub event: E, } +/* +pub trait TimedEffect { + fn duration(&self) -> LocalCycles; +} + +pub struct TickContext +where + E: TimedEffect, + C: ClockedComponent, +{ + pub elapsed: GlobalDuration, + pub context: E, + _marker: PhantomData, +} + +pub trait TickListener: ClockedComponent { + type Context: TimedEffect; + type Effect; + + fn on_tick( + &mut self, + context: TickContext, + ) -> Result, Self::Fault>; +} +*/ + /// Generic boundary for a component that participates in a global event loop. /// /// The trait shares only clock vocabulary with `GlobalClock`: ticks, instruction timing, and /// owned scheduling requests. It does not depend on a particular clock implementation or root /// event enum. Components supply their own instruction, event, completion, and fault types. -pub trait ClockedComponent { +pub trait ClockedComponent: Sized { type Event; type Completion; type Fault; diff --git a/demos/examples/demo/stdlib/counter.rs b/demos/examples/demo/stdlib/counter.rs new file mode 100644 index 00000000..1af52349 --- /dev/null +++ b/demos/examples/demo/stdlib/counter.rs @@ -0,0 +1,161 @@ +use std::{collections::HashMap, convert::Infallible}; + +use vihaco::{Execute, NoEffect, NoMessage, StepResult}; + +vihaco::component! { + pub component CounterGroup { + queued: Vec, + playing: HashMap, + next_id: CounterId, + } + + instruction { + #[derive(Debug, Clone)] + Queue { start: u32, duration: u32 }, + #[derive(Debug, Clone)] + Play, + } +} + +use counter_group::*; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct CounterId(pub u32); + +impl std::ops::AddAssign for CounterId { + fn add_assign(&mut self, rhs: u32) { + self.0 += rhs + } +} + +struct QueuedCounter { + id: CounterId, + count: u32, + duration: u32, +} + +struct PlayedCounter { + count: u32, + time_left: u32, +} + +impl QueuedCounter { + fn increase(&mut self) { + self.count += 1; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CounterResult { + Advanced(u32), + Done(u32), +} + +#[derive(Debug, Default)] +pub struct PlayReport { + pub results: HashMap, +} + +impl CounterGroup { + pub fn new() -> Self { + Self { + queued: vec![], + playing: HashMap::new(), + next_id: CounterId(0), + } + } + + fn queue(&mut self, start: u32, duration: u32) { + self.queued.push(QueuedCounter { + id: self.next_id, + count: start, + duration, + }); + self.next_id += 1; + } + + /// Move queued channels into the active set. The machine owns the clock; this component + /// only owns channel lifetime and evaluation state. + pub fn play(&mut self) -> usize { + let played = self.queued.drain(..).collect::>(); + let mut started = 0; + + for c in played { + if c.duration == 0 { + continue; + } + let new_c = PlayedCounter { + count: c.count, + time_left: c.duration, + }; + self.playing.insert(c.id, new_c); + started += 1; + } + + started + } + + /// Evaluate every active channel once. A future waveform implementation can use the same + /// boundary while accepting an elapsed-time/sample-rate argument. + pub fn advance(&mut self) -> PlayReport { + let mut results = HashMap::new(); + + let ids = self.playing.keys().copied().collect::>(); + for id in ids { + let counter = self + .playing + .get_mut(&id) + .expect("active counter disappeared during advancement"); + counter.count += 1; + counter.time_left -= 1; + + if counter.time_left == 0 { + results.insert(id, CounterResult::Done(counter.count)); + self.playing.remove(&id); + } else { + results.insert(id, CounterResult::Advanced(counter.count)); + } + } + + PlayReport { results } + } + + pub fn is_playing(&self) -> bool { + !self.playing.is_empty() + } +} + +impl Execute for CounterGroup { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = Infallible; + + fn execute( + &mut self, + instruction: &instruction::Queue, + _message: Self::Message, + ) -> Result, Self::Fault> { + self.queue(instruction.start, instruction.duration); + vihaco::complete!() + } +} + +#[derive(Debug)] +pub struct Playing { + pub channels: usize, +} + +impl Execute for CounterGroup { + type Message = NoMessage; + type Effect = Playing; + type Fault = Infallible; + + fn execute( + &mut self, + _instruction: &instruction::Play, + _message: Self::Message, + ) -> Result, Self::Fault> { + let channels = self.play(); + vihaco::complete!(Playing { channels }) + } +} diff --git a/demos/examples/demo/stdlib/debug_trace.rs b/demos/examples/demo/stdlib/debug_trace.rs index 6a1e420b..6e78e027 100644 --- a/demos/examples/demo/stdlib/debug_trace.rs +++ b/demos/examples/demo/stdlib/debug_trace.rs @@ -1,7 +1,10 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -use super::{Effects, handle::Observe}; +use super::{ + Effects, + handle::{Absorb, Observe}, +}; vihaco::component! { component DebugTrace { @@ -17,12 +20,27 @@ impl debug_trace::DebugTrace { records: Vec::new(), } } + + fn record(&mut self, route: &'static str, effect: &E) { + self.records.push(DebugRecord { + route, + effect: format!("{effect:?}"), + }); + } + + /// Record an effect produced by a clock-driven component together with its global tick. + pub fn record_at(&mut self, tick: u64, effect: &E) { + self.records.push(DebugRecord { + route: "clock", + effect: format!("tick {tick}: {effect:?}"), + }); + } } #[derive(Debug)] pub struct DebugRecord { - route: &'static str, - effect: String, + pub route: &'static str, + pub effect: String, } impl Observe for debug_trace::DebugTrace @@ -34,10 +52,19 @@ where type Error = std::convert::Infallible; fn observe(&mut self, effect: &E) -> Result, Self::Error> { - self.records.push(DebugRecord { - route: std::any::type_name::(), - effect: format!("{effect:?}"), - }); + self.record(std::any::type_name::(), effect); Ok(Effects::none()) } } + +impl Absorb for debug_trace::DebugTrace +where + E: std::fmt::Debug, +{ + type Fault = std::convert::Infallible; + + fn absorb(&mut self, effect: E) -> Result<(), Self::Fault> { + self.record("absorbed", &effect); + Ok(()) + } +} diff --git a/demos/examples/demo/vihaco/handle.rs b/demos/examples/demo/vihaco/handle.rs index 12e24d5b..0cfe5728 100644 --- a/demos/examples/demo/vihaco/handle.rs +++ b/demos/examples/demo/vihaco/handle.rs @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +#[allow(unused_imports)] pub use vihaco::{Absorb, Handle, Observe}; From 2496a5b51144810535ee81dae77680c0a97ba7d6 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Mon, 10 Aug 2026 12:40:47 -0400 Subject: [PATCH 07/15] Split runtime_instructions into `syntax` and `runtime` blocks in `composite!` macro; added generated syntax resolution traits for composites --- crates/vihaco-cpu/src/instruction.rs | 58 +++++-- crates/vihaco-module/src/loader.rs | 76 +++++++++ crates/vihaco-parser-derive/src/attr.rs | 15 +- crates/vihaco-parser-derive/src/codegen.rs | 38 ++--- .../compile_errors/pattern_duplicate_index.rs | 2 +- .../tests/compile_errors/pattern_empty.rs | 2 +- .../pattern_index_out_of_bounds.rs | 2 +- .../compile_errors/pattern_index_overflow.rs | 2 +- .../pattern_instruction_without_token.rs | 2 +- .../pattern_invalid_binding_identifier.rs | 2 +- .../compile_errors/pattern_leading_space.rs | 2 +- .../compile_errors/pattern_missing_index.rs | 2 +- .../compile_errors/pattern_mixed_bindings.rs | 2 +- .../compile_errors/pattern_repeated_space.rs | 2 +- .../compile_errors/pattern_tab_separator.rs | 2 +- .../pattern_tab_separator.stderr | 2 +- .../compile_errors/pattern_trailing_space.rs | 2 +- .../pattern_tuple_named_binding.rs | 2 +- .../pattern_unsupported_symbol.rs | 2 +- .../pattern_unterminated_literal.rs | 2 +- crates/vihaco-parser-derive/tests/patterns.rs | 116 ++++++------- crates/vihaco-parser-derive/tests/struct.rs | 4 +- .../design/composite_macro.md | 8 +- crates/vihaco-runtime-derive/src/composite.rs | 38 ++++- .../src/composite/codegen.rs | 159 +++++++++++++++++- .../src/composite/syntax.rs | 86 +++++++++- .../src/composite/validate.rs | 55 +++++- crates/vihaco-syntax/src/lib.rs | 7 +- crates/vihaco/src/lib.rs | 11 +- .../duplicate-effect-handler.rs | 22 +++ .../duplicate-effect-handler.stderr | 5 + .../duplicate-message-clause.rs | 18 ++ .../duplicate-message-clause.stderr | 5 + .../composite_machine/duplicate-observer.rs | 23 +++ .../duplicate-observer.stderr | 5 + .../duplicate-route-variant.rs | 20 +++ .../duplicate-route-variant.stderr | 5 + .../missing-effects-handler.rs | 2 +- .../missing-message-clause.rs | 15 ++ .../missing-message-clause.stderr | 5 + .../composite_machine/unknown-route-fields.rs | 21 +++ .../unknown-route-fields.stderr | 5 + .../unsupported-to-clause.rs | 18 ++ .../unsupported-to-clause.stderr | 5 + .../tests/composite_machine_compile_fail.rs | 7 + crates/vihaco/tests/multisection_bytecode.rs | 7 +- .../tests/runtime_macro_crate_override.rs | 17 +- demos/examples/counter-machine/src/machine.rs | 2 +- demos/examples/demo-vihaco-concepts.md | 2 +- demos/examples/demo/src/cpu.rs | 2 +- demos/src/main.rs | 9 +- docs/examples/quickstart_parse.rs | 8 +- docs/src/pages/guide/composites.md | 4 +- docs/src/pages/guide/parser-advanced.md | 10 +- docs/src/pages/guide/parser-patterns.md | 23 +-- docs/src/pages/guide/parser.md | 26 +-- vision/execution-pipeline.md | 10 +- vision/macro-generation.md | 8 +- vision/sst-resolution.md | 2 +- 59 files changed, 807 insertions(+), 207 deletions(-) create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.stderr create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.rs create mode 100644 crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.stderr diff --git a/crates/vihaco-cpu/src/instruction.rs b/crates/vihaco-cpu/src/instruction.rs index e470775a..88934eea 100644 --- a/crates/vihaco-cpu/src/instruction.rs +++ b/crates/vihaco-cpu/src/instruction.rs @@ -144,117 +144,145 @@ pub enum SurfaceValue { } #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] -#[syntax_class(instruction, head = "cpu")] +#[syntax_class(instruction)] pub enum SurfaceInstruction { // no-ops /// span /// `span 0 1 2` — three space-separated u32s. - #[pattern = "'span $0 $1 $2"] + #[pattern = "'cpu::span $0 $1 $2"] Span(u32, u32, u32), /// Label definition. - #[pattern = "'label `@` $0"] + #[pattern = "'cpu::label `@` $0"] Label(Ident), /// `func_start ` — marks function entry. `` is symbolic and /// orchestrator-resolved; the unit variant carries no payload. - #[pattern = "'func_start"] + #[pattern = "'cpu::func_start"] FunctionStart, /// `func_end ` — marks function exit (debug only). - #[pattern = "'func_end"] + #[pattern = "'cpu::func_end"] FunctionEnd, /// `breakpoint`. Must precede `Branch` (whose token `br` would be a /// prefix of `breakpoint`). + #[pattern = "'cpu::breakpoint"] Breakpoint, // control flows /// `br ` — symbolic. Deferred to orchestrator. - #[pattern = "'br `@` $0"] + #[pattern = "'cpu::br `@` $0"] Branch(Ident), /// `cond_br , ` — symbolic. Deferred. - #[pattern = "'cond_br `@` $0 `,` `@` $1"] + #[pattern = "'cpu::cond_br `@` $0 `,` `@` $1"] ConditionalBranch(Ident, Ident), /// `ret` (bare) is the form real `.sst` uses; numeric `ret ` has no /// precedent so we defer. Orchestrator emits `Return(0)` for bare `ret`. - #[pattern = "'ret"] + #[pattern = "'cpu::ret"] Return, /// `call_indirect`. **Must precede `Call`** for the prefix check. - #[pattern = "'call_indirect"] + #[pattern = "'cpu::call_indirect"] IndirectCall, /// `call , ` — symbolic addr. Deferred. + #[pattern = "'cpu::call $0 `,` $1"] Call(u32, Ident), /// `halt` — stop execution. + #[pattern = "'cpu::halt"] Halt, // traps / IO /// `print` — write top-of-stack to stdout. + #[pattern = "'cpu::print"] Print, // memory operations /// `load.
` — two fields with single-space separator. + #[pattern = "'cpu::load $0 `,` $1"] Load(SurfaceType, u32), /// `store.
`. + #[pattern = "'cpu::store $0 `,` $1"] Store(SurfaceType, u32), /// `dup`. + #[pattern = "'cpu::dup"] Dup, /// `heap_alloc `. - #[pattern = "'heap_alloc $0"] + #[pattern = "'cpu::heap_alloc $0"] HeapAlloc(u32), /// `get_item`. Must precede `Ge` (token `ge` ⊂ `get_item`). - #[pattern = "'get_item"] + #[pattern = "'cpu::get_item"] GetItem, /// `heap_dealloc` — pops a HeapRef and marks the slot dead, returning it /// to the free list for reuse by the next `heap_alloc`. - #[pattern = "'heap_dealloc"] + #[pattern = "'cpu::heap_dealloc"] HeapDealloc, /// `const. ` — numeric/bool only here. `.str`/`.fn_ref`/ /// `.heap_ref` are orchestrator-handled. + #[pattern = "'cpu::const $0 `,` $1"] Const(SurfaceType, SurfaceValue), // arithmetic operations + #[pattern = "'cpu::add $0"] Add(SurfaceType), + #[pattern = "'cpu::sub $0"] Sub(SurfaceType), + #[pattern = "'cpu::mul $0"] Mul(SurfaceType), + #[pattern = "'cpu::div $0"] Div(SurfaceType), + #[pattern = "'cpu::rem $0"] Rem(SurfaceType), + #[pattern = "'cpu::neg $0"] Neg(SurfaceType), // integer / bitwise operations + #[pattern = "'cpu::shl $0"] Shl(SurfaceType), + #[pattern = "'cpu::shr $0"] Shr(SurfaceType), + #[pattern = "'cpu::rol $0"] Rol(SurfaceType), + #[pattern = "'cpu::ror $0"] Ror(SurfaceType), - #[pattern = "'bitand $0"] + #[pattern = "'cpu::bitand $0"] BitAnd(SurfaceType), - #[pattern = "'bitor $0"] + #[pattern = "'cpu::bitor $0"] BitOr(SurfaceType), - #[pattern = "'bitxor $0"] + #[pattern = "'cpu::bitxor $0"] BitXor(SurfaceType), // boolean operations + #[pattern = "'cpu::not"] Not, + #[pattern = "'cpu::and"] And, + #[pattern = "'cpu::or"] Or, + #[pattern = "'cpu::xor"] Xor, // comparison operations + #[pattern = "'cpu::eq $0"] Eq(SurfaceType), + #[pattern = "'cpu::ne $0"] Ne(SurfaceType), + #[pattern = "'cpu::lt $0"] Lt(SurfaceType), + #[pattern = "'cpu::gt $0"] Gt(SurfaceType), + #[pattern = "'cpu::le $0"] Le(SurfaceType), + #[pattern = "'cpu::ge $0"] Ge(SurfaceType), } diff --git a/crates/vihaco-module/src/loader.rs b/crates/vihaco-module/src/loader.rs index af295faa..742e2cbd 100644 --- a/crates/vihaco-module/src/loader.rs +++ b/crates/vihaco-module/src/loader.rs @@ -50,6 +50,21 @@ pub trait LoadSstSection { fn load_sst_section<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; } +/// Replace a program's loaded module, context, and program-counter state as one operation. +/// +/// Implementations must complete any validation before mutating the program. The supplied +/// context becomes the context for the installed module, and a successful installation starts +/// execution at program counter zero. +pub trait InstallProgramModule { + type Module; + + fn install_program_module( + &mut self, + module: Self::Module, + context: ContextHandle, + ) -> eyre::Result<()>; +} + #[derive(Debug, Clone)] pub struct ProgramImage { pub module: LocalModule, @@ -100,6 +115,21 @@ impl ProgramImage { } } +impl InstallProgramModule for ProgramImage { + type Module = LocalModule; + + fn install_program_module( + &mut self, + module: Self::Module, + context: ContextHandle, + ) -> eyre::Result<()> { + self.module = module; + self.context = Some(context); + self.pc = 0; + Ok(()) + } +} + impl ProgramCounter for ProgramImage { type Instruction = I; @@ -159,3 +189,49 @@ where }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Clone, PartialEq)] + struct TestInstruction(u32); + + #[derive(Debug, Clone, PartialEq)] + struct TestType; + + #[derive(Debug, Clone, Default, PartialEq)] + struct TestInfo; + + #[test] + fn installation_replaces_module_context_and_pc_together() { + let old_context = ContextHandle::new("old"); + let new_context = ContextHandle::new("new"); + let mut image: ProgramImage = ProgramImage { + module: LocalModule { + code: vec![TestInstruction(1)], + extra: TestInfo, + ..LocalModule::default() + }, + context: Some(old_context), + pc: 7, + }; + let module = LocalModule { + code: vec![TestInstruction(2), TestInstruction(3)], + extra: TestInfo, + ..LocalModule::default() + }; + + image + .install_program_module(module, new_context.clone()) + .unwrap(); + + assert_eq!( + image.module.code, + vec![TestInstruction(2), TestInstruction(3)] + ); + assert_eq!(image.context().unwrap(), &"new"); + assert!(image.context.as_ref().unwrap().ptr_eq(&new_context)); + assert_eq!(image.pc, 0); + } +} diff --git a/crates/vihaco-parser-derive/src/attr.rs b/crates/vihaco-parser-derive/src/attr.rs index 8c8a688e..ece6b026 100644 --- a/crates/vihaco-parser-derive/src/attr.rs +++ b/crates/vihaco-parser-derive/src/attr.rs @@ -9,7 +9,6 @@ use syn::{ }; mod kw { - syn::custom_keyword!(head); syn::custom_keyword!(instruction); syn::custom_keyword!(value); } @@ -18,7 +17,7 @@ mod kw { #[derive(Clone)] pub enum SyntaxClassAttr { - Instruction { head: String }, + Instruction, Type, Value, } @@ -27,17 +26,7 @@ impl Parse for SyntaxClassAttr { fn parse(input: ParseStream<'_>) -> Result { if input.peek(kw::instruction) { input.parse::()?; - - if input.is_empty() { - return Err(input.error("instruction syntax class must have `head` argument")); - } - - input.parse::()?; - input.parse::()?; - input.parse::()?; - let head = input.parse::()?.value(); - - return Ok(Self::Instruction { head }); + return Ok(Self::Instruction); } if input.peek(kw::value) { diff --git a/crates/vihaco-parser-derive/src/codegen.rs b/crates/vihaco-parser-derive/src/codegen.rs index 65302af3..c1e4e517 100644 --- a/crates/vihaco-parser-derive/src/codegen.rs +++ b/crates/vihaco-parser-derive/src/codegen.rs @@ -101,7 +101,11 @@ fn pattern_syntax_parser<'p>( let ident = text::ascii::ident(); let digits = text::int(10); - let token = just('\'').ignore_then(ident).map(Token); + let instruction_name = text::ascii::ident() + .then(just("::").ignore_then(text::ascii::ident()).repeated()) + .to_slice(); + + let token = just('\'').ignore_then(instruction_name).map(Token); let binding_index = digits .to_slice() @@ -232,7 +236,7 @@ impl<'p> UnparsedPatternInfo<'p> { let pattern = PatternAtoms::try_new(tokens)?; - if matches!(self.class, SyntaxClassAttr::Instruction { .. }) + if matches!(self.class, SyntaxClassAttr::Instruction) && !matches!(pattern.first(), Token(_)) { return Err(eyre::eyre!( @@ -240,7 +244,7 @@ impl<'p> UnparsedPatternInfo<'p> { )); } - if !matches!(self.class, SyntaxClassAttr::Instruction { .. }) { + if !matches!(self.class, SyntaxClassAttr::Instruction) { if let Some(tok) = pattern.contains_token() { return Err(eyre::eyre!( "cannot have instruction syntax '{tok} in {} pattern", @@ -268,7 +272,7 @@ impl<'p> UnparsedPatternInfo<'p> { impl fmt::Display for SyntaxClassAttr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Instruction { .. } => write!(f, "instruction"), + Self::Instruction => write!(f, "instruction"), Self::Type => write!(f, "type"), Self::Value => write!(f, "value"), } @@ -694,7 +698,7 @@ fn generate_pattern<'src>( } let name = info.target.ident().to_string().to_lowercase(); - let prefix = if matches!(info.class, Some(SyntaxClassAttr::Instruction { .. })) { + let prefix = if matches!(info.class, Some(SyntaxClassAttr::Instruction)) { Some(format!("'{}", name)) } else { None @@ -877,20 +881,11 @@ fn expand_enum(input: EnumInfo) -> Result { quote! { ::chumsky::primitive::choice((#(#chunks),*)) } }; - let parser = match &enum_attrs.syntax_class { - Some(SyntaxClassAttr::Instruction { head }) => { - let head = format!("{head}::"); - quote! { - ::chumsky::primitive::just(#head) - .ignore_then(#or_chain) - } - } - _ => or_chain, - }; + let parser = or_chain; let surface_instruction_impl = if matches!( &enum_attrs.syntax_class, - Some(SyntaxClassAttr::Instruction { .. }) + Some(SyntaxClassAttr::Instruction) ) { let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); quote! { @@ -968,16 +963,7 @@ fn expand_struct(input: StructInfo) -> Result { let (impl_generics, _, where_clause) = parser_generics.split_for_impl(); let (_, ty_generics, _) = input.generics.split_for_impl(); - let parser = match &struct_attrs.syntax_class { - Some(SyntaxClassAttr::Instruction { head }) => { - let head = format!("{head}::"); - quote! { - ::chumsky::primitive::just(#head) - .ignore_then(#ident) - } - } - _ => quote! { #ident }, - }; + let parser = quote! { #ident }; let output = quote! { impl #impl_generics ::vihaco_parser::Parse<#src_lifetime> for #struct_ident #ty_generics #where_clause { diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_duplicate_index.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_duplicate_index.rs index 1c4cc3f5..a7141ad9 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_duplicate_index.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_duplicate_index.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'pair $0 $0"] Pair(i64, bool), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_empty.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_empty.rs index 9f47fc07..2aff2141 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_empty.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_empty.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = ""] Halt, diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_out_of_bounds.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_out_of_bounds.rs index 539c98e1..8a2d2670 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_out_of_bounds.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_out_of_bounds.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'pair $0 $2"] Pair(i64, bool), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_overflow.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_overflow.rs index e6723d17..141567c4 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_overflow.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_index_overflow.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load $4294967296"] Load(i64), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_instruction_without_token.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_instruction_without_token.rs index c5c2f330..8405770c 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_instruction_without_token.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_instruction_without_token.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "`load` $0"] Load(i64), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_invalid_binding_identifier.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_invalid_binding_identifier.rs index 60438418..3752221a 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_invalid_binding_identifier.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_invalid_binding_identifier.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load $-field"] Load(i64), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_leading_space.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_leading_space.rs index 9b448ed6..63f19f24 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_leading_space.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_leading_space.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = " 'load"] Load, diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_missing_index.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_missing_index.rs index e09d9670..e877e139 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_missing_index.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_missing_index.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'pair $0"] Pair(i64, bool), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_mixed_bindings.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_mixed_bindings.rs index 178dff9b..5c449250 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_mixed_bindings.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_mixed_bindings.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'pair $0 $right"] Pair(i64, bool), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_repeated_space.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_repeated_space.rs index 95bbad84..5e1be919 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_repeated_space.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_repeated_space.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load $0"] Load(i64), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.rs index 0b9d5b6d..6c413900 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load\t$0"] Load(i64), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.stderr b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.stderr index ab4e6e48..434345f4 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.stderr +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tab_separator.stderr @@ -1,4 +1,4 @@ -error: invalid pattern: found ' ' expected identifier, ' ', or end of input +error: invalid pattern: found ' ' expected identifier, ':', ' ', or end of input --> tests/compile_errors/pattern_tab_separator.rs:9:17 | 9 | #[pattern = "'load\t$0"] diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_trailing_space.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_trailing_space.rs index 4d886530..7f2ee07a 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_trailing_space.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_trailing_space.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load "] Load, diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tuple_named_binding.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tuple_named_binding.rs index c0bf5b3e..2606f424 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_tuple_named_binding.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_tuple_named_binding.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'pair $left $right"] Pair(i64, bool), diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_unsupported_symbol.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_unsupported_symbol.rs index b81cc847..1f6a096a 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_unsupported_symbol.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_unsupported_symbol.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load `;`"] Load, diff --git a/crates/vihaco-parser-derive/tests/compile_errors/pattern_unterminated_literal.rs b/crates/vihaco-parser-derive/tests/compile_errors/pattern_unterminated_literal.rs index 9c663c0a..ee37868a 100644 --- a/crates/vihaco-parser-derive/tests/compile_errors/pattern_unterminated_literal.rs +++ b/crates/vihaco-parser-derive/tests/compile_errors/pattern_unterminated_literal.rs @@ -4,7 +4,7 @@ use vihaco_parser_derive::Parse; #[derive(Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load `comma"] Load, diff --git a/crates/vihaco-parser-derive/tests/patterns.rs b/crates/vihaco-parser-derive/tests/patterns.rs index 26b714f9..501cf3f5 100644 --- a/crates/vihaco-parser-derive/tests/patterns.rs +++ b/crates/vihaco-parser-derive/tests/patterns.rs @@ -30,7 +30,7 @@ impl<'src> ParseTrait<'src> for Operand { } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum PermutedTuple { #[pattern = "'p012 $0 $1 $2"] P012(i64, bool, Ident), @@ -49,27 +49,27 @@ enum PermutedTuple { #[test] fn tuple_bindings_are_assigned_by_index_not_capture_order() { assert_eq!( - parse("test::p012 7 true word"), + parse("p012 7 true word"), Ok(PermutedTuple::P012(7, true, ident("word"))) ); assert_eq!( - parse("test::p021 7 word true"), + parse("p021 7 word true"), Ok(PermutedTuple::P021(7, true, ident("word"))) ); assert_eq!( - parse("test::p102 true 7 word"), + parse("p102 true 7 word"), Ok(PermutedTuple::P102(7, true, ident("word"))) ); assert_eq!( - parse("test::p120 true word 7"), + parse("p120 true word 7"), Ok(PermutedTuple::P120(7, true, ident("word"))) ); assert_eq!( - parse("test::p201 word 7 true"), + parse("p201 word 7 true"), Ok(PermutedTuple::P201(7, true, ident("word"))) ); assert_eq!( - parse("test::p210 word true 7"), + parse("p210 word true 7"), Ok(PermutedTuple::P210(7, true, ident("word"))) ); } @@ -94,7 +94,7 @@ fn named_bindings_are_assigned_by_name_not_capture_order() { } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Punctuation { #[pattern = "'comma $0 `,` $1"] Comma(i64, bool), @@ -106,55 +106,49 @@ enum Punctuation { #[test] fn comma_suppresses_only_leading_whitespace() { + assert_eq!(parse("comma 1, true"), Ok(Punctuation::Comma(1, true))); assert_eq!( - parse("test::comma 1, true"), - Ok(Punctuation::Comma(1, true)) - ); - assert_eq!( - parse("test::comma 1, false"), + parse("comma 1, false"), Ok(Punctuation::Comma(1, false)) ); - assert!(parse::("test::comma 1 , true").is_err()); - assert!(parse::("test::comma 1,true").is_err()); + assert!(parse::("comma 1 , true").is_err()); + assert!(parse::("comma 1,true").is_err()); } #[test] fn at_suppresses_only_trailing_whitespace() { assert_eq!( - parse("test::at 1 @target"), + parse("at 1 @target"), Ok(Punctuation::At(1, ident("target"))) ); assert_eq!( - parse("test::at 1 @target"), + parse("at 1 @target"), Ok(Punctuation::At(1, ident("target"))) ); - assert!(parse::("test::at 1@target").is_err()); - assert!(parse::("test::at 1 @ target").is_err()); + assert!(parse::("at 1@target").is_err()); + assert!(parse::("at 1 @ target").is_err()); } #[test] fn ordinary_atoms_require_ascii_spaces_and_exact_literals() { + assert_eq!(parse("wrapped before 9 after"), Ok(Punctuation::Wrapped(9))); assert_eq!( - parse("test::wrapped before 9 after"), - Ok(Punctuation::Wrapped(9)) - ); - assert_eq!( - parse("test::wrapped before 9 after"), + parse("wrapped before 9 after"), Ok(Punctuation::Wrapped(9)) ); for invalid in [ - "test::wrapped before9 after", - "test::wrapped before 9after", - "test::wrapped\tbefore 9 after", - "test::wrapped before\n9 after", - "test::wrapped wrong 9 after", - "test::wrapped before 9 wrong", - "prefix test::wrapped before 9 after", - "test::wrapped before 9 after suffix", - "test::wrapped before nope after", + "wrapped before9 after", + "wrapped before 9after", + "wrapped\tbefore 9 after", + "wrapped before\n9 after", + "wrapped wrong 9 after", + "wrapped before 9 wrong", + "prefix wrapped before 9 after", + "wrapped before 9 after suffix", + "wrapped before nope after", ] { assert!( parse::(invalid).is_err(), @@ -164,14 +158,14 @@ fn ordinary_atoms_require_ascii_spaces_and_exact_literals() { } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum GeneratedInstruction { Halt, Move(i64, bool), } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum ExplicitInstruction { #[pattern = "'halt"] Halt, @@ -181,34 +175,34 @@ enum ExplicitInstruction { #[test] fn generated_instruction_patterns_match_equivalent_explicit_patterns() { - assert_eq!(parse("test::halt"), Ok(GeneratedInstruction::Halt)); - assert_eq!(parse("test::halt"), Ok(ExplicitInstruction::Halt)); + assert_eq!(parse("halt"), Ok(GeneratedInstruction::Halt)); + assert_eq!(parse("halt"), Ok(ExplicitInstruction::Halt)); assert_eq!( - parse("test::move 12, true"), + parse("move 12, true"), Ok(GeneratedInstruction::Move(12, true)) ); assert_eq!( - parse("test::move 12, true"), + parse("move 12, true"), Ok(ExplicitInstruction::Move(12, true)) ); } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "analog")] +#[syntax_class(instruction)] enum AnalogDialect { - #[pattern = "'set $0"] + #[pattern = "'analog::set $0"] Set(i64), } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "digital")] +#[syntax_class(instruction)] enum DigitalDialect { - #[pattern = "'set $0"] + #[pattern = "'digital::set $0"] Set(i64), } #[test] -fn instruction_heads_select_the_dialect() { +fn complete_instruction_tokens_select_the_dialect() { assert_eq!(parse("analog::set 3"), Ok(AnalogDialect::Set(3))); assert_eq!(parse("digital::set 5"), Ok(DigitalDialect::Set(5))); @@ -268,7 +262,7 @@ fn generated_named_field_pattern_matches_an_explicit_pattern() { } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum Instruction { #[pattern = "'load $0"] Load(i64), @@ -300,7 +294,7 @@ fn instruction_list<'src>() -> impl Parser< #[test] fn parses_a_newline_separated_instruction_source() { - let source = "test::load 4\ntest::store destination, 8\ntest::jump 2 @loop\ntest::halt\n"; + let source = "load 4\nstore destination, 8\njump 2 @loop\nhalt\n"; assert_eq!( instruction_list().parse(source).into_result(), @@ -315,7 +309,7 @@ fn parses_a_newline_separated_instruction_source() { #[test] fn instruction_list_rejects_a_bad_instruction_without_losing_neighbors() { - let source = "test::load 4\ntest::store destination 8\ntest::halt"; + let source = "load 4\nstore destination 8\nhalt"; assert!(instruction_list().parse(source).has_errors()); } @@ -343,7 +337,7 @@ where T: for<'a> ParseTrait<'a>; #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "generic")] +#[syntax_class(instruction)] enum GenericInstruction where T: for<'a> ParseTrait<'a>, @@ -361,17 +355,14 @@ fn generics_and_generated_lifetime_name_collisions_compile_and_parse() { Ok(LifetimeCollision(31, Marker(PhantomData))) ); assert_eq!(parse("37"), Ok(Generic(37_i64))); - assert_eq!( - parse("generic::value 41"), - Ok(GenericInstruction::Value(41_i64)) - ); + assert_eq!(parse("value 41"), Ok(GenericInstruction::Value(41_i64))); require_surface_instruction::>(); } macro_rules! define_instruction_enum { ($name:ident { $($variant:ident),+ $(,)? }) => { #[derive(Parse, Debug, PartialEq)] - #[syntax_class(instruction, head = "test")] + #[syntax_class(instruction)] enum $name { $($variant),+ } @@ -495,24 +486,21 @@ define_instruction_enum!(FiftyThreeVariants { #[test] fn enum_choice_boundaries_compile_and_select_the_right_variant() { - assert_eq!(parse("test::v0"), Ok(OneVariant::V0)); - assert_eq!(parse("test::v1"), Ok(TwoVariants::V1)); - assert_eq!(parse("test::v25"), Ok(TwentySixVariants::V25)); - assert_eq!(parse("test::v26"), Ok(TwentySevenVariants::V26)); - assert_eq!(parse("test::v52"), Ok(FiftyThreeVariants::V52)); + assert_eq!(parse("v0"), Ok(OneVariant::V0)); + assert_eq!(parse("v1"), Ok(TwoVariants::V1)); + assert_eq!(parse("v25"), Ok(TwentySixVariants::V25)); + assert_eq!(parse("v26"), Ok(TwentySevenVariants::V26)); + assert_eq!(parse("v52"), Ok(FiftyThreeVariants::V52)); } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum AcronymInstruction { HttpServer, } #[test] fn generated_names_are_lowercase() { - assert_eq!( - parse("test::httpserver"), - Ok(AcronymInstruction::HttpServer) - ); - assert!(parse::("test::HttpServer").is_err()); + assert_eq!(parse("httpserver"), Ok(AcronymInstruction::HttpServer)); + assert!(parse::("HttpServer").is_err()); } diff --git a/crates/vihaco-parser-derive/tests/struct.rs b/crates/vihaco-parser-derive/tests/struct.rs index 8d7c37c2..d572b410 100644 --- a/crates/vihaco-parser-derive/tests/struct.rs +++ b/crates/vihaco-parser-derive/tests/struct.rs @@ -14,8 +14,8 @@ struct Named { } #[derive(Parse, Debug, PartialEq)] -#[syntax_class(instruction, head = "test")] -#[pattern = "'pair $0 $1"] +#[syntax_class(instruction)] +#[pattern = "'test::pair $0 $1"] struct Tuple(i64, bool); #[derive(Parse, Debug, PartialEq)] diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md index f8f96b5b..92a75ef7 100644 --- a/crates/vihaco-runtime-derive/design/composite_macro.md +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -34,7 +34,7 @@ The macro should: - consume each effect through exactly one handler; - support reusable `Absorb` delegation and composite-owned custom handlers; - normalize component, observer, and handler errors into the composite error; and -- support structural composites that omit `runtime_instructions` entirely. +- support structural composites that omit `runtime` entirely. The generated execution boundary is an inherent method: @@ -140,7 +140,7 @@ composite! { pub pc: usize, } - runtime_instructions { + runtime { IntegerAdd(Add) => alu { message from operand_stack; effects { @@ -160,7 +160,7 @@ composite! { } ``` -The `runtime_instructions` block is optional. If it is omitted, the composite takes no +The `runtime` block is optional. If it is omitted, the composite takes no instructions: the macro generates the struct and metadata/section wiring, but no instruction enum and no `execute_generated` method. @@ -342,7 +342,7 @@ keeps route-specific resolution explicit without introducing a route-parameteriz ## Structural composites A structural composite may contain clocks, fabrics, devices, or other runtime state but omit -`runtime_instructions`: +`runtime`: ```rust composite! { diff --git a/crates/vihaco-runtime-derive/src/composite.rs b/crates/vihaco-runtime-derive/src/composite.rs index 1eb802cd..f78cdd4f 100644 --- a/crates/vihaco-runtime-derive/src/composite.rs +++ b/crates/vihaco-runtime-derive/src/composite.rs @@ -33,7 +33,7 @@ mod tests { alu: Alu, debug: Debug, } - runtime_instructions { + runtime { Add(AddInstruction) => alu { message from stack; effects { @@ -83,7 +83,7 @@ mod tests { error = CounterMachineFault; counter_group: CounterGroup, } - runtime_instructions { + runtime { Queue(QueueInstruction) => counter_group { message none; } @@ -96,4 +96,38 @@ mod tests { assert!(declaration.routes[0].observers.is_empty()); assert!(declaration.routes[0].handler.is_none()); } + + #[test] + fn parses_syntax_entries_and_direct_runtime_mappings() { + let declaration: CompositeDeclaration = parse_str( + r#" + composite Machine { + error = MachineFault; + device: Device, + } + syntax { + #[pattern = "'device::clear"] + Clear => runtime Clear; + #[pattern = "'device::set $0"] + Set(u32) => lower_set; + } + runtime { + Clear(DeviceInstruction) => device { + message none; + } + } + "#, + ) + .unwrap(); + + assert_eq!(declaration.syntax.len(), 2); + assert!(matches!( + declaration.syntax[0].mapping, + super::syntax::SyntaxMapping::Runtime(_) + )); + assert!(matches!( + declaration.syntax[1].mapping, + super::syntax::SyntaxMapping::Lower(_) + )); + } } diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index adac8bbf..dff5e6d8 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -1,11 +1,15 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use convert_case::{Case, Casing}; use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote, quote_spanned}; use syn::{Field, Generics, Ident, Result, Type}; -use super::syntax::{CompositeDeclaration, Handler, MessageSource, RouteDeclaration}; +use super::syntax::{ + CompositeDeclaration, Handler, MessageSource, RouteDeclaration, SyntaxDeclaration, + SyntaxMapping, +}; use crate::common::{resolve_root, retain_generics}; pub(super) fn retained_enum_generics(generics: &Generics, routes: &[RouteDeclaration]) -> Generics { @@ -34,9 +38,117 @@ fn strip_consumed_field_attrs(mut field: Field) -> Field { field } +fn syntax_generics(generics: &Generics, syntax: &[SyntaxDeclaration]) -> Generics { + let payloads = syntax + .iter() + .filter_map(|entry| entry.payload.as_ref()) + .map(|payload| quote!(#payload)) + .collect::>(); + retain_generics(generics, &payloads) +} + +fn generate_syntax_module( + root: &TokenStream2, + generics: &Generics, + error: Option<&Type>, + syntax: &[SyntaxDeclaration], +) -> TokenStream2 { + if syntax.is_empty() { + return quote! {}; + } + + let enum_generics = syntax_generics(generics, syntax); + let variants = syntax.iter().map(|entry| { + let variant = &entry.variant; + let pattern = &entry.pattern; + let payload = entry + .payload + .as_ref() + .map(|payload| quote!((#payload))) + .unwrap_or_default(); + quote! { + #[pattern = #pattern] + #variant #payload + } + }); + let error = error + .map(|error| quote!(#error)) + .unwrap_or_else(|| quote!(::core::convert::Infallible)); + let lowerer_methods = syntax.iter().filter_map(|entry| { + let SyntaxMapping::Lower(method) = &entry.mapping else { + return None; + }; + let payload = entry.payload.as_ref()?; + Some(quote! { + fn #method( + &mut self, + instruction: #payload, + ) -> ::std::result::Result< + ::std::vec::Vec, + #error, + >; + }) + }); + + quote! { + pub mod syntax { + use super::*; + #[derive(Clone, #root::Parse)] + #[syntax_class(instruction)] + pub enum Instruction #enum_generics { + #( #variants ),* + } + + pub trait Resolver { + #( #lowerer_methods )* + } + } + } +} + +fn generate_resolver_traits( + root: &TokenStream2, + generics: &Generics, + error: Option<&Type>, + routes: &[RouteDeclaration], + fields: &[super::validate::FieldMetadata], +) -> TokenStream2 { + let Some(error) = error else { + return quote! {}; + }; + let field_ty = |field: &Ident| -> &Type { + &fields + .iter() + .find(|candidate| candidate.ident == *field) + .expect("validated composite field") + .ty + }; + let message_methods = routes.iter().filter_map(|route| { + let MessageSource::With(method) = &route.message else { + return None; + }; + let target_ty = field_ty(&route.target); + let payload = &route.payload; + let message_ty = quote!(<#target_ty as #root::Execute<#payload>>::Message); + Some(quote! { + fn #method( + &mut self, + instruction: &#payload, + ) -> ::std::result::Result<#message_ty, #error>; + }) + }); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + quote! { + pub trait MessageResolver #impl_generics #where_clause { + #( #message_methods )* + } + } +} + pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result { let root = resolve_root(&declaration.attrs)?; let fields_metadata = super::validate::metadata_fields(&declaration.fields)?; + super::validate::validate_syntax(&declaration.syntax, &declaration.routes)?; super::validate::validate_routes(&declaration.routes, &fields_metadata)?; let CompositeDeclaration { @@ -46,6 +158,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result Result Result Result, pub(super) fields: Vec, + pub(super) syntax: Vec, pub(super) routes: Vec, } +pub(super) struct SyntaxDeclaration { + pub(super) pattern: LitStr, + pub(super) variant: Ident, + pub(super) payload: Option, + pub(super) mapping: SyntaxMapping, +} + +pub(super) enum SyntaxMapping { + Lower(Ident), + Runtime(Ident), +} + pub(super) struct RouteDeclaration { pub(super) variant: Ident, pub(super) payload: Type, @@ -69,8 +84,17 @@ impl Parse for CompositeDeclaration { syn::braced!(body in input); let (error, fields) = parse_composite_body(&body)?; - let routes = if input.peek(runtime_instructions) { - input.parse::()?; + let syntax = if input.peek(syntax) { + input.parse::()?; + let syntax_body; + syn::braced!(syntax_body in input); + parse_syntax(&syntax_body)? + } else { + Vec::new() + }; + + let routes = if input.peek(runtime) { + input.parse::()?; let routes_body; syn::braced!(routes_body in input); parse_routes(&routes_body)? @@ -96,11 +120,67 @@ impl Parse for CompositeDeclaration { generics, error, fields, + syntax, routes, }) } } +fn parse_syntax(input: ParseStream<'_>) -> Result> { + let mut declarations = Vec::new(); + while !input.is_empty() { + let attrs = Attribute::parse_outer(input)?; + let [pattern] = attrs.as_slice() else { + return Err(input.error("syntax entries require one `#[pattern = \"...\"]` attribute")); + }; + if !pattern.path().is_ident("pattern") { + return Err(syn::Error::new( + pattern.span(), + "syntax entries require `#[pattern = \"...\"]`", + )); + } + let value = &pattern.meta.require_name_value()?.value; + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(pattern), + .. + }) = value + else { + return Err(syn::Error::new( + input.span(), + "`pattern` must be a string literal", + )); + }; + + let variant = input.parse::()?; + let payload = if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + if content.is_empty() { + None + } else { + Some(content.parse()?) + } + } else { + None + }; + input.parse::]>()?; + let mapping = if input.peek(runtime) { + input.parse::()?; + SyntaxMapping::Runtime(input.parse()?) + } else { + SyntaxMapping::Lower(input.parse()?) + }; + input.parse::()?; + declarations.push(SyntaxDeclaration { + pattern: pattern.clone(), + variant, + payload, + mapping, + }); + } + Ok(declarations) +} + fn parse_composite_body(input: ParseStream<'_>) -> Result<(Option, Vec)> { let mut error_type = None; diff --git a/crates/vihaco-runtime-derive/src/composite/validate.rs b/crates/vihaco-runtime-derive/src/composite/validate.rs index 0b3ea731..e4f3ba37 100644 --- a/crates/vihaco-runtime-derive/src/composite/validate.rs +++ b/crates/vihaco-runtime-derive/src/composite/validate.rs @@ -6,7 +6,9 @@ use std::collections::{BTreeMap, BTreeSet}; use syn::spanned::Spanned; use syn::{Field, Ident, LitStr, Result, Type}; -use super::syntax::{DeviceArgs, Handler, MessageSource, RouteDeclaration}; +use super::syntax::{ + DeviceArgs, Handler, MessageSource, RouteDeclaration, SyntaxDeclaration, SyntaxMapping, +}; pub(super) struct FieldMetadata { pub(super) ident: Ident, @@ -187,3 +189,54 @@ pub(super) fn validate_routes(routes: &[RouteDeclaration], fields: &[FieldMetada } Ok(()) } + +pub(super) fn validate_syntax( + syntax: &[SyntaxDeclaration], + routes: &[RouteDeclaration], +) -> Result<()> { + let mut variants = BTreeSet::new(); + let mut patterns = BTreeSet::new(); + let route_variants: BTreeSet<_> = routes + .iter() + .map(|route| route.variant.to_string()) + .collect(); + + for entry in syntax { + if !variants.insert(entry.variant.to_string()) { + return Err(syn::Error::new( + entry.variant.span(), + format!("duplicate syntax instruction variant `{}`", entry.variant), + )); + } + if !patterns.insert(entry.pattern.value()) { + return Err(syn::Error::new( + entry.pattern.span(), + "duplicate syntax instruction pattern", + )); + } + match &entry.mapping { + SyntaxMapping::Runtime(runtime_variant) => { + if entry.payload.is_some() { + return Err(syn::Error::new( + entry.variant.span(), + "direct runtime mappings may only be used for unit syntax instructions", + )); + } + if !route_variants.contains(&runtime_variant.to_string()) { + return Err(syn::Error::new( + runtime_variant.span(), + format!("unknown runtime route `{runtime_variant}`"), + )); + } + } + SyntaxMapping::Lower(lowerer) if entry.payload.is_none() => { + return Err(syn::Error::new( + lowerer.span(), + "named syntax lowerers require an instruction payload", + )); + } + SyntaxMapping::Lower(_) => {} + } + } + Ok(()) +} diff --git a/crates/vihaco-syntax/src/lib.rs b/crates/vihaco-syntax/src/lib.rs index c1b90c5f..95d13035 100644 --- a/crates/vihaco-syntax/src/lib.rs +++ b/crates/vihaco-syntax/src/lib.rs @@ -29,9 +29,11 @@ mod tests { // Minimal stub: an enum that derives Parse and has just two unit variants. // Avoids pulling vihaco-cpu/-fpga into the test (cycle). #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] - #[syntax_class(instruction, head = "stub")] + #[syntax_class(instruction)] enum StubInst { + #[pattern = "'stub::halt"] Halt, + #[pattern = "'stub::print"] Print, } @@ -118,8 +120,9 @@ fn @main() { #[test] fn rejects_malformed_known_instruction() { #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] - #[syntax_class(instruction, head = "stub")] + #[syntax_class(instruction)] enum OnlyOne { + #[pattern = "'stub::dump $0"] Dump(u32), } diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index 1d121b46..8ab5b86e 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -36,7 +36,8 @@ pub use instruction_syntax::{ InstructionSugarVariantSyntax, OperandKind, SugarOperandKind, }; pub use loader::{ - LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, LoadSstSection, ProgramImage, + InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, + LoadSstSection, ProgramImage, }; pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; @@ -47,13 +48,15 @@ pub use runtime::{ }; pub use traits::{FromBytes, FromText, GetProgramInfo, Reset}; pub use vihaco_parser::SurfaceInstruction; +pub use vihaco_parser_derive::Parse; #[cfg(test)] mod public_api_tests { use crate::{ BytecodeGlobalContext, BytecodeHeader, ConstantId, EffectSink, Effects, Execute, Execution, - GlobalContext, LoadBytecodeSection, LoadOwnBytecodeSection, Reset, SectionNameResolver, - SstGlobalContext, SstHeader, StepResult, WriteBytecodeHeader, + GlobalContext, InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, + ProgramImage, Reset, SectionNameResolver, SstGlobalContext, SstHeader, StepResult, + WriteBytecodeHeader, instruction::{FromBytes, OpCode, WriteBytes}, module::FunctionInfo, observer::stdio::StdoutEffect, @@ -127,6 +130,7 @@ mod public_api_tests { fn require_global_context() {} fn require_load_own_bytecode_section>() {} fn require_load_bytecode_section>() {} + fn require_install_program_module>() {} fn require_stdout_effect(_effect: StdoutEffect) {} fn require_metadata(_metadata: crate::CompositeMetadata) {} @@ -143,6 +147,7 @@ mod public_api_tests { require_global_context::(); require_load_own_bytecode_section::(); require_load_bytecode_section::(); + require_install_program_module::>(); let _constant = ConstantId(0); let _function: Option> = None; require_stdout_effect(StdoutEffect(String::new())); diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.rs new file mode 100644 index 00000000..eb7112aa --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.rs @@ -0,0 +1,22 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + sink: Sink, + } + + runtime { + Run(Instruction) => target { + message none; + effects { + absorb with sink; + handle with handle_effect; + } + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.stderr new file mode 100644 index 00000000..3515db21 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-effect-handler.stderr @@ -0,0 +1,5 @@ +error: route has more than one effect handler + --> tests/compile_fail/composite_machine/duplicate-effect-handler.rs:16:29 + | +16 | handle with handle_effect; + | ^^^^^^^^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.rs new file mode 100644 index 00000000..beb56388 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.rs @@ -0,0 +1,18 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + } + + runtime { + Run(Instruction) => target { + message none; + message none; + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.stderr new file mode 100644 index 00000000..18d54a76 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-message-clause.stderr @@ -0,0 +1,5 @@ +error: route has more than one message clause + --> tests/compile_fail/composite_machine/duplicate-message-clause.rs:13:21 + | +13 | message none; + | ^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.rs new file mode 100644 index 00000000..3281ab7f --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.rs @@ -0,0 +1,23 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + observer: Observer, + sink: Sink, + } + + runtime { + Run(Instruction) => target { + message none; + effects { + observe observer, observer; + absorb with sink; + } + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.stderr new file mode 100644 index 00000000..c8970da6 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-observer.stderr @@ -0,0 +1,5 @@ +error: duplicate observer field `observer` + --> tests/compile_fail/composite_machine/duplicate-observer.rs:16:35 + | +16 | observe observer, observer; + | ^^^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.rs new file mode 100644 index 00000000..502c0216 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.rs @@ -0,0 +1,20 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + } + + runtime { + Run(Instruction) => target { + message none; + } + Run(Instruction) => target { + message none; + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.stderr new file mode 100644 index 00000000..4e5594dd --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-route-variant.stderr @@ -0,0 +1,5 @@ +error: duplicate runtime instruction variant `Run` + --> tests/compile_fail/composite_machine/duplicate-route-variant.rs:14:9 + | +14 | Run(Instruction) => target { + | ^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs index da2a0a7d..29273aa4 100644 --- a/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs +++ b/crates/vihaco/tests/compile_fail/composite_machine/missing-effects-handler.rs @@ -37,7 +37,7 @@ vihaco::composite! { target: Target, } - runtime_instructions { + runtime { Queue(Instruction) => target { message none; } diff --git a/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.rs b/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.rs new file mode 100644 index 00000000..2bda5c50 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.rs @@ -0,0 +1,15 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + } + + runtime { + Run(Instruction) => target {} + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.stderr b/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.stderr new file mode 100644 index 00000000..88675858 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/missing-message-clause.stderr @@ -0,0 +1,5 @@ +error: route is missing a message clause + --> tests/compile_fail/composite_machine/missing-message-clause.rs:11:9 + | +11 | Run(Instruction) => target {} + | ^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.rs b/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.rs new file mode 100644 index 00000000..3133f2cf --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.rs @@ -0,0 +1,21 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + } + + runtime { + Run(Instruction) => missing_target { + message from missing_message; + effects { + observe missing_observer; + absorb with missing_sink; + } + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.stderr b/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.stderr new file mode 100644 index 00000000..f2078a53 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/unknown-route-fields.stderr @@ -0,0 +1,5 @@ +error: unknown composite field `missing_target` + --> tests/compile_fail/composite_machine/unknown-route-fields.rs:11:29 + | +11 | Run(Instruction) => missing_target { + | ^^^^^^^^^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.rs b/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.rs new file mode 100644 index 00000000..642b5aa2 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.rs @@ -0,0 +1,18 @@ +struct Target; +struct Instruction; + +vihaco::composite! { + composite Machine { + error = Fault; + target: Target, + } + + runtime { + Run(Instruction) => target { + message none; + effects to target; + } + } +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.stderr b/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.stderr new file mode 100644 index 00000000..441d5db7 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/unsupported-to-clause.stderr @@ -0,0 +1,5 @@ +error: expected curly braces + --> tests/compile_fail/composite_machine/unsupported-to-clause.rs:13:21 + | +13 | effects to target; + | ^^ diff --git a/crates/vihaco/tests/composite_machine_compile_fail.rs b/crates/vihaco/tests/composite_machine_compile_fail.rs index 5b6ca4eb..1c3ba1cf 100644 --- a/crates/vihaco/tests/composite_machine_compile_fail.rs +++ b/crates/vihaco/tests/composite_machine_compile_fail.rs @@ -9,4 +9,11 @@ fn composite_machine_rejects_ambiguous_wiring() { t.compile_fail("tests/compile_fail/composite_machine/invalid-loadable-name.rs"); t.compile_fail("tests/compile_fail/composite_machine/loadable-without-device.rs"); t.compile_fail("tests/compile_fail/composite_machine/missing-effects-handler.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-route-variant.rs"); + t.compile_fail("tests/compile_fail/composite_machine/missing-message-clause.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-message-clause.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-effect-handler.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-observer.rs"); + t.compile_fail("tests/compile_fail/composite_machine/unknown-route-fields.rs"); + t.compile_fail("tests/compile_fail/composite_machine/unsupported-to-clause.rs"); } diff --git a/crates/vihaco/tests/multisection_bytecode.rs b/crates/vihaco/tests/multisection_bytecode.rs index fb1f8702..f1241129 100644 --- a/crates/vihaco/tests/multisection_bytecode.rs +++ b/crates/vihaco/tests/multisection_bytecode.rs @@ -31,15 +31,18 @@ enum TestInst { } #[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser_derive::Parse)] -#[syntax_class(instruction, head = "test")] +#[syntax_class(instruction)] enum TextInst { + #[pattern = "'test::nop"] Nop, + #[pattern = "'test::alt"] Alt, } #[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] -#[syntax_class(instruction, head = "surface")] +#[syntax_class(instruction)] enum SurfaceOnlyInst { + #[pattern = "'surface::nop"] Nop, } diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index e1bac194..21d0b474 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -1,8 +1,10 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT +use chumsky::Parser as _; use eyre::Result; use vihaco::{Effects, Execute, Execution, Instruction, Observe, StepResult, composite}; +use vihaco_parser::Parse; mod test_root { pub use ::vihaco::*; @@ -59,7 +61,14 @@ composite! { observer: TestObserver, } - runtime_instructions { + syntax { + #[pattern = "'test::run"] + Run => runtime Run; + #[pattern = "'test::count $0"] + Count(u32) => lower_count; + } + + runtime { Run(TestInstruction) => component { message with resolve_message; effects { @@ -82,6 +91,12 @@ impl TestMachine { #[test] fn runtime_macros_honor_explicit_crate_override() { + let parsed = test_machine::syntax::Instruction::parser() + .parse("test::run") + .into_result() + .unwrap(); + assert!(matches!(parsed, test_machine::syntax::Instruction::Run)); + let mut machine = TestMachine { component: TestComponent, observer: TestObserver::default(), diff --git a/demos/examples/counter-machine/src/machine.rs b/demos/examples/counter-machine/src/machine.rs index 42067da7..755f6cd0 100644 --- a/demos/examples/counter-machine/src/machine.rs +++ b/demos/examples/counter-machine/src/machine.rs @@ -36,7 +36,7 @@ vihaco::composite! { advance_scheduled: bool, } - runtime_instructions { + runtime { Queue(counter_group::instruction::Queue) => counter_group { message none; } diff --git a/demos/examples/demo-vihaco-concepts.md b/demos/examples/demo-vihaco-concepts.md index 6700315d..6b807a9f 100644 --- a/demos/examples/demo-vihaco-concepts.md +++ b/demos/examples/demo-vihaco-concepts.md @@ -52,7 +52,7 @@ composite! { alu: ArithmeticUnit, } - runtime_instructions { + runtime { IntegerAdd(Add) => alu { message from operand_stack; effects { absorb with operand_stack; } diff --git a/demos/examples/demo/src/cpu.rs b/demos/examples/demo/src/cpu.rs index df823a1b..12d6899a 100644 --- a/demos/examples/demo/src/cpu.rs +++ b/demos/examples/demo/src/cpu.rs @@ -30,7 +30,7 @@ vihaco::composite! { pub pc: usize, } - runtime_instructions { + runtime { IntegerAdd(Add) => alu { message from operand_stack; effects { diff --git a/demos/src/main.rs b/demos/src/main.rs index 448b7804..d07de2df 100644 --- a/demos/src/main.rs +++ b/demos/src/main.rs @@ -109,9 +109,11 @@ mod channel { use_vihaco_parse!(); #[derive(Parse)] - #[syntax_class(instruction, head = "channel")] + #[syntax_class(instruction)] pub enum Instruction { + #[pattern = "'channel::send $0"] Send(u32), + #[pattern = "'channel::recv $0"] Recv(u32), } @@ -282,13 +284,16 @@ mod arithmetic { use_vihaco_parse!(); #[derive(Parse)] - #[syntax_class(instruction, head = "arith")] + #[syntax_class(instruction)] pub enum Instruction where Ty: for<'a> Parse<'a>, { + #[pattern = "'arith::add $0"] Add(Ty), + #[pattern = "'arith::sub $0"] Sub(Ty), + #[pattern = "'arith::mul $0"] Mul(Ty), } diff --git a/docs/examples/quickstart_parse.rs b/docs/examples/quickstart_parse.rs index d056483a..c74ef7b4 100644 --- a/docs/examples/quickstart_parse.rs +++ b/docs/examples/quickstart_parse.rs @@ -5,16 +5,16 @@ use vihaco_parser::Parse; // The same enum can derive both `Instruction` (bytecode + runtime) and // `Parse` (SST). The two derives are orthogonal. #[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser_derive::Parse)] -#[syntax_class(instruction, head = "counter")] +#[syntax_class(instruction)] pub enum CounterInst { - #[pattern = "'add $0"] + #[pattern = "'counter::add $0"] Add(i64), Print, } fn main() { - // The syntax class supplies the `counter::` namespace. Patterns bind - // source operands directly to Rust fields. + // Patterns include the complete source token and bind operands directly + // to Rust fields. let inst = CounterInst::parser() .parse("counter::add 5") .into_result() diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index 9530f706..fe61db5f 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -66,7 +66,7 @@ composite! { trace: Trace, } - runtime_instructions { + runtime { Add(Add) => arithmetic { message from stack; effects { @@ -121,7 +121,7 @@ that owns program data implements `LoadOwnBytecodeSection` or `LoadOwnSstSection` in ordinary Rust. The composite macro can also declare structural composites with no -`runtime_instructions` block. Those composites still provide fields, device +`runtime` block. Those composites still provide fields, device metadata, and section wiring, while their event loop or parent dispatch remains hand-written. diff --git a/docs/src/pages/guide/parser-advanced.md b/docs/src/pages/guide/parser-advanced.md index eb1ae95b..c6f34d5d 100644 --- a/docs/src/pages/guide/parser-advanced.md +++ b/docs/src/pages/guide/parser-advanced.md @@ -69,10 +69,10 @@ use vihaco::Instruction; use vihaco_parser_derive::Parse; #[derive(Debug, Clone, PartialEq, Instruction, Parse)] -#[syntax_class(instruction, head = "device")] +#[syntax_class(instruction)] enum DeviceInstruction { Halt, - #[pattern = "'wait $0"] + #[pattern = "'device::wait $0"] Wait(u32), } @@ -176,11 +176,11 @@ Patterns can represent symbols and sugar directly: use vihaco_parser::Ident; #[derive(vihaco_parser_derive::Parse)] -#[syntax_class(instruction, head = "control")] +#[syntax_class(instruction)] enum ControlSurface { - #[pattern = "'branch `@` $0"] + #[pattern = "'control::branch `@` $0"] Branch(Ident), - #[pattern = "'repeat $0"] + #[pattern = "'control::repeat $0"] Repeat(u32), } ``` diff --git a/docs/src/pages/guide/parser-patterns.md b/docs/src/pages/guide/parser-patterns.md index 14d4f366..3c7c78aa 100644 --- a/docs/src/pages/guide/parser-patterns.md +++ b/docs/src/pages/guide/parser-patterns.md @@ -27,12 +27,13 @@ use vihaco_parser_derive::Parse; use vihaco_parser::Parse as ParseTrait; #[derive(Debug, PartialEq, Parse)] -#[syntax_class(instruction, head = "memory")] +#[syntax_class(instruction)] enum MemoryInstruction { + #[pattern = "'memory::halt"] Halt, - #[pattern = "'load $0"] + #[pattern = "'memory::load $0"] Load(u32), - #[pattern = "'store $0 `,` $1"] + #[pattern = "'memory::store $0 `,` $1"] Store(u32, i64), } @@ -55,9 +56,8 @@ assert_eq!( ); ``` -The `head` is a dialect namespace. The derive appends `::`, so -`head = "memory"` combines with the pattern token `'load` to accept -`memory::load`. +Instruction patterns contain the complete dialect namespace. For example, +`'memory::load $0` accepts `memory::load 4`. Every bound field is parsed with that field type's `vihaco_parser::Parse::parser()`. Give domain-specific field syntax its @@ -69,12 +69,13 @@ Every type using pattern generation must declare a syntax class. | Attribute | Meaning | Additional rules | |---|---|---| -| `#[syntax_class(instruction, head = "dialect")]` | An instruction in the `dialect::` namespace. | Every pattern starts with an instruction token such as `'load`. | +| `#[syntax_class(instruction)]` | An instruction whose patterns contain complete tokens such as `dialect::load`. | Every pattern starts with a complete instruction token. | | `#[syntax_class(value)]` | A value expression. | Instruction tokens are forbidden. Simple defaults are available. | | `#[syntax_class(type)]` | A type expression. | Instruction tokens are forbidden and every variant or struct needs an explicit pattern. | Put `#[syntax_class]` on the enum or struct definition, never on a variant or -field. An instruction head is required and is written without trailing `::`. +field. Instruction patterns carry their complete source token, including any +namespace. ## Generated patterns @@ -84,9 +85,9 @@ split (`HttpServer` becomes `httpserver`). | Rust shape | Generated pattern | Accepted source | |---|---|---| -| instruction `Halt` | `'halt` | `dialect::halt` | -| instruction `Move(i64, bool)` | 'move $0 `,` $1 | `dialect::move 3, true` | -| instruction struct `Set { x: i64, enabled: bool }` | 'set $x `,` $enabled | `dialect::set 3, true` | +| instruction `Halt` | `'halt` | `halt` | +| instruction `Move(i64, bool)` | 'move $0 `,` $1 | `move 3, true` | +| instruction struct `Set { x: i64, enabled: bool }` | 'set $x `,` $enabled | `set 3, true` | | value `Nothing` | `` `nothing` `` | `nothing` | | value `Number(i64)` | `$0` | `3` | | value `Wrapper { value: i64 }` | `$value` | `3` | diff --git a/docs/src/pages/guide/parser.md b/docs/src/pages/guide/parser.md index 0d86fef0..63c33406 100644 --- a/docs/src/pages/guide/parser.md +++ b/docs/src/pages/guide/parser.md @@ -33,10 +33,11 @@ use vihaco_parser::Parse as ParseTrait; #[derive(Debug, Clone, PartialEq, Instruction, Parse)] #[instruction(width = 8)] -#[syntax_class(instruction, head = "counter")] +#[syntax_class(instruction)] enum CounterInstruction { - #[pattern = "'add $0"] + #[pattern = "'counter::add $0"] Add(i64), + #[pattern = "'counter::print"] Print, } @@ -59,10 +60,11 @@ The two derives are independent: - `Instruction` defines bytecode encoding and runtime opcode behavior. - `Parse` defines source syntax. -`#[syntax_class(instruction, head = "counter")]` places every instruction in -the `counter::` namespace. A unit variant receives a conventional lowercase -pattern automatically. `#[pattern = "'add $0"]` spells out the mnemonic and -binds the first tuple field. The derive also implements +`#[syntax_class(instruction)]` marks an instruction parser. Each instruction +pattern contains its complete source token, including any namespace. A unit +variant receives a conventional lowercase pattern automatically. +`#[pattern = "'counter::add $0"]` spells out the complete mnemonic and binds +the first tuple field. The derive also implements `SurfaceInstruction` for an instruction-class enum. ## The `Parse` trait @@ -102,7 +104,7 @@ Every derived parser declares exactly one syntax class: | Attribute | Role | |---|---| -| `#[syntax_class(instruction, head = "dialect")]` | A namespaced instruction such as `dialect::load` | +| `#[syntax_class(instruction)]` | An instruction whose patterns contain complete tokens such as `dialect::load` | | `#[syntax_class(value)]` | A value expression | | `#[syntax_class(type)]` | A type expression with an explicit pattern | @@ -126,11 +128,11 @@ use vihaco_parser_derive::Parse; use vihaco_parser::{Ident, Parse as ParseTrait}; #[derive(Debug, PartialEq, Parse)] -#[syntax_class(instruction, head = "control")] +#[syntax_class(instruction)] enum ControlInstruction { - #[pattern = "'branch `@` $0"] + #[pattern = "'control::branch `@` $0"] Branch(Ident), - #[pattern = "'select $0 `,` $1"] + #[pattern = "'control::select $0 `,` $1"] Select(bool, u32), } @@ -168,9 +170,9 @@ use vihaco_parser::{Ident, Parse as ParseTrait}; struct Address(Ident); #[derive(Debug, PartialEq, Parse)] -#[syntax_class(instruction, head = "control")] +#[syntax_class(instruction)] enum ControlInstruction { - #[pattern = "'branch `@` $0"] + #[pattern = "'control::branch `@` $0"] Branch(Address), } diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md index 61e60e54..1072fa65 100644 --- a/vision/execution-pipeline.md +++ b/vision/execution-pipeline.md @@ -323,7 +323,7 @@ describes semantic meaning, but it does not by itself identify a destination wit ##### Route Identity -Each entry in a composite's `runtime_instructions` declaration defines one route. Its +Each entry in a composite's `runtime` declaration defines one route. Its composite-local name identifies the complete execution path: ```text @@ -352,7 +352,7 @@ source operands are resolved. A typed addition illustrates the distinction: ```rust #[derive(vihaco_parser::Parse)] -#[syntax_class(instruction, head = "arithmetic")] +#[syntax_class(instruction)] #[pattern = "'add $ty"] pub struct SurfaceAdd { pub ty: ArithmeticSurfaceType, @@ -372,7 +372,7 @@ arithmetic::add address The composite can provide distinct runtime routes for the supported resolved types: ```rust -runtime_instructions { +runtime { IntegerAdd => arithmetic::runtime::Add on integer_arithmetic { message from operand_stack; effects to operand_stack; @@ -444,7 +444,7 @@ never choose conversion semantics. The same `Add` runtime instruction can therefore appear through two routes: ```rust -runtime_instructions { +runtime { IntegerAdd => arithmetic::runtime::Add on integer_arithmetic { message from operand_stack; effects to operand_stack; @@ -477,7 +477,7 @@ composite assigns destinations and machine-local behavior. ##### Generated Route Representation -`IntegerAdd` and `AddressAdd` originate in the composite's `runtime_instructions` declaration. +`IntegerAdd` and `AddressAdd` originate in the composite's `runtime` declaration. Generation turns them into variants of the machine runtime sum: ```rust diff --git a/vision/macro-generation.md b/vision/macro-generation.md index 0500a83f..95e57f1c 100644 --- a/vision/macro-generation.md +++ b/vision/macro-generation.md @@ -19,7 +19,7 @@ For example: ```rust #[derive(vihaco_parser::Parse)] -#[syntax_class(instruction, head = "control")] +#[syntax_class(instruction)] #[pattern = "'branch `@` $target"] pub struct SurfaceBranch { pub target: String, @@ -111,7 +111,7 @@ machine! { fpga: Fpga, } - runtime_instructions { + runtime { device Cpu => cpu::Instruction { message with resolve_cpu; effects with continue_cpu; @@ -129,7 +129,7 @@ The generated portion is equivalent in shape to: ```rust #[composite] -#[runtime_instructions( +#[runtime( Cpu => cpu::Instruction { message with resolve_cpu; effects with continue_cpu; @@ -169,7 +169,7 @@ machine! { cpu_b: Cpu, } - runtime_instructions {} + runtime {} } ``` diff --git a/vision/sst-resolution.md b/vision/sst-resolution.md index b973087b..e85e514d 100644 --- a/vision/sst-resolution.md +++ b/vision/sst-resolution.md @@ -5,7 +5,7 @@ runtime instructions: ```rust #[derive(vihaco_parser::Parse)] -#[syntax_class(instruction, head = "control")] +#[syntax_class(instruction)] #[pattern = "'conditional_branch `@` $when_true `,` `@` $when_false"] pub struct SurfaceConditionalBranch { pub when_true: String, From 52926dc870ade4714b6398a9534f6149ef149462 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Mon, 10 Aug 2026 13:20:32 -0400 Subject: [PATCH 08/15] Updated module building; added support for `#[program]`; separated `ProgramCounter` from the `Instruction` trait; added support for resolving and loading parsed modules --- README.md | 10 +- crates/vihaco-module/src/host.rs | 3 +- crates/vihaco-module/src/loader.rs | 146 ++++++++++- .../design/composite_macro.md | 20 +- .../src/composite/codegen.rs | 226 +++++++++++++++++- .../src/composite/validate.rs | 23 ++ crates/vihaco/src/lib.rs | 4 +- .../tests/runtime_macro_crate_override.rs | 52 +++- docs/src/pages/guide/composites.md | 71 ++++++ docs/src/pages/guide/parser-advanced.md | 13 + vision/execution-pipeline.md | 21 ++ 11 files changed, 562 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2a2a0fcb..b9d0bc24 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,10 @@ vihaco is a framework for building small virtual machines. You define - reusable **components** and their instruction products with `component!`; - one `Execute` implementation per product, with typed messages and effects; -- **composite routes** with `composite!`; and -- (optionally) **SST source syntax** with the parser derives, +- **composite routes** with `composite!`; +- executable composite **surface syntax and program loading** with the parser + derives; and +- (optionally) standalone **SST source syntax** with the parser derives, all as ordinary Rust. A component step is `execute(&instruction, message) -> StepResult`: @@ -91,8 +93,8 @@ No mise? A stable Rust 2024 toolchain is enough — `cargo test --workspace Guides and the API reference are published to GitHub Pages: ****. The guides walk through defining -instructions, pattern parser integration, messages, components, observers, and -composites. +instructions, pattern parser integration, typed module resolution, messages, +components, observers, composites, and composite-owned program loading. Every code block in the guides and on the site is compiled — and, where runnable, executed — in CI (via the `vihaco-doctests` crate), so the examples diff --git a/crates/vihaco-module/src/host.rs b/crates/vihaco-module/src/host.rs index 0e44de65..aea4d277 100644 --- a/crates/vihaco-module/src/host.rs +++ b/crates/vihaco-module/src/host.rs @@ -4,13 +4,12 @@ use eyre::Result; use vihaco_abi::frame::Frame; -use vihaco_abi::traits::Instruction; use vihaco_bytecode::ConstantId; use crate::module::FunctionInfo; pub trait ProgramCounter { - type Instruction: Instruction; + type Instruction; /// Get the current program counter. fn pc(&self) -> u32; diff --git a/crates/vihaco-module/src/loader.rs b/crates/vihaco-module/src/loader.rs index 742e2cbd..658acf9a 100644 --- a/crates/vihaco-module/src/loader.rs +++ b/crates/vihaco-module/src/loader.rs @@ -2,11 +2,10 @@ // SPDX-License-Identifier: MIT use vihaco_abi::program::{Type, Value}; -use vihaco_abi::traits::Instruction; use vihaco_bytecode::{BytecodeSectionView, ConstantId, ContextHandle, SstSectionView}; use crate::host::{GetProgramInfo, ProgramCounter}; -use crate::module::{LocalModule, NoInfo}; +use crate::module::{FunctionInfo, LabelInfo, LocalModule, NoInfo, SourceSymbolInfo}; /// Allow a machine to load the bytecode data owned directly by its section. /// @@ -65,6 +64,42 @@ pub trait InstallProgramModule { ) -> eyre::Result<()>; } +/// Build the runtime module owned by a program container from resolved SST data. +/// +/// [`LocalModule`] receives the standard implementation through [`ProgramImage`]. Custom +/// program containers may implement this capability to preserve their own module and metadata +/// representation while still using generated composite loading. +pub trait BuildProgramModule { + type Instruction; + type Value; + type Type; + type Info; + type Module; + + fn empty_module() -> Self::Module; + + fn append_instructions( + module: &mut Self::Module, + instructions: impl IntoIterator, + ); + + fn instruction_count(module: &Self::Module) -> u32; + + fn add_function(module: &mut Self::Module, function: FunctionInfo); + + fn add_label(module: &mut Self::Module, label: LabelInfo); + + fn add_source_symbol(module: &mut Self::Module, symbol: SourceSymbolInfo); + + fn intern_string(module: &mut Self::Module, value: String) -> u32; + + fn add_constant(module: &mut Self::Module, value: Self::Value) -> u32; + + fn set_main_function(module: &mut Self::Module, function: Option); + + fn finish(module: Self::Module) -> eyre::Result; +} + #[derive(Debug, Clone)] pub struct ProgramImage { pub module: LocalModule, @@ -130,7 +165,70 @@ impl InstallProgramModule for ProgramImage ProgramCounter for ProgramImage { +impl BuildProgramModule for ProgramImage { + type Instruction = I; + type Value = V; + type Type = Ty; + type Info = Info; + type Module = LocalModule; + + fn empty_module() -> Self::Module { + LocalModule::default() + } + + fn append_instructions( + module: &mut Self::Module, + instructions: impl IntoIterator, + ) { + module.code.extend(instructions); + } + + fn instruction_count(module: &Self::Module) -> u32 { + module.code.len() as u32 + } + + fn add_function(module: &mut Self::Module, function: FunctionInfo) { + module.functions.push(function); + } + + fn add_label(module: &mut Self::Module, label: LabelInfo) { + module.labels.push(label); + } + + fn add_source_symbol(module: &mut Self::Module, symbol: SourceSymbolInfo) { + module.source_symbols.push(symbol); + } + + fn intern_string(module: &mut Self::Module, value: String) -> u32 { + if let Some(index) = module + .strings + .iter() + .position(|existing| existing == &value) + { + index as u32 + } else { + let index = module.strings.len() as u32; + module.strings.push(value); + index + } + } + + fn add_constant(module: &mut Self::Module, value: Self::Value) -> u32 { + let index = module.constants.len() as u32; + module.constants.push(value); + index + } + + fn set_main_function(module: &mut Self::Module, function: Option) { + module.main_function = function; + } + + fn finish(module: Self::Module) -> eyre::Result { + Ok(module) + } +} + +impl ProgramCounter for ProgramImage { type Instruction = I; fn pc(&self) -> u32 { @@ -152,7 +250,7 @@ impl ProgramCounter for ProgramImage GetProgramInfo for ProgramImage +impl GetProgramInfo for ProgramImage where Ty: Clone, { @@ -234,4 +332,44 @@ mod tests { assert!(image.context.as_ref().unwrap().ptr_eq(&new_context)); assert_eq!(image.pc, 0); } + + #[test] + fn program_counter_supports_non_bytecode_instructions() { + let mut image: ProgramImage = ProgramImage { + module: LocalModule { + code: vec![TestInstruction(7), TestInstruction(11)], + ..LocalModule::default() + }, + context: None, + pc: 0, + }; + + assert_eq!(image.peek_instruction().unwrap(), &TestInstruction(7)); + assert_eq!(image.next_instruction().unwrap(), &TestInstruction(7)); + assert_eq!(image.pc(), 1); + assert_eq!(image.next_instruction().unwrap(), &TestInstruction(11)); + assert_eq!(image.pc(), 2); + } + + #[test] + fn local_module_builder_appends_and_interns() { + let mut module = + as BuildProgramModule>::empty_module(); + as BuildProgramModule>::append_instructions( + &mut module, + [TestInstruction(7), TestInstruction(11)], + ); + let first = as BuildProgramModule>::intern_string( + &mut module, + "main".to_owned(), + ); + let second = as BuildProgramModule>::intern_string( + &mut module, + "main".to_owned(), + ); + + assert_eq!(module.code, vec![TestInstruction(7), TestInstruction(11)]); + assert_eq!(first, 0); + assert_eq!(second, first); + } } diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md index 92a75ef7..98d50040 100644 --- a/crates/vihaco-runtime-derive/design/composite_macro.md +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -2,9 +2,10 @@ ## Status -Phase-one implementation plan. This document records the agreed runtime-only scope for the -author-facing `composite!` macro. Surface parsing, module resolution, bytecode, and scheduling -remain later work. +This document records the implemented author-facing `composite!` macro and +the remaining boundaries. Runtime routing, composite-owned surface parsing, +parsed-module lowering, and program installation are implemented. Bytecode +encoding and scheduling remain separate concerns. ## Purpose @@ -27,6 +28,10 @@ The macro should: - declare an explicit composite error type in the macro input; - preserve existing `#[device(...)]` and `#[loadable]` metadata and validation; - generate a public `Instruction` enum for executable composites; +- generate an optional `syntax` namespace with pattern-derived surface instructions; +- generate named surface-instruction resolver methods for payload-bearing syntax; +- build and install a marked `#[program]` field through `BuildProgramModule` and + `InstallProgramModule`; - generate private route marker types and route-specific trait implementations; - resolve messages using `none`, `from`, or a composite-owned resolver method; - execute selected component instructions through `Execute`; @@ -52,8 +57,8 @@ resolution code can construct it. Phase one does not: -- generate surface instruction parsers; -- generate or implement `Resolve` for parsed modules; +- make `ProgramCounter` depend on bytecode encoding traits; +- require every program container to use `LocalModule` storage; - generate bytecode codecs; - fetch instructions or own a program counter; - generate resume or continuation dispatch; @@ -181,8 +186,9 @@ fpga: Fpga, ``` `#[loadable]` continues to identify device fields that participate in generated bytecode/SST -section loading. `#[program]` may remain accepted as a marker for later program plumbing, but has -no phase-one execution semantics. +section loading. `#[program]` marks the program container used by generated parsed-module +loading. The container must implement `BuildProgramModule` and +`InstallProgramModule`; `ProgramImage` supplies the standard implementation. ### Runtime route declaration diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index dff5e6d8..c0de113d 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -145,6 +145,205 @@ fn generate_resolver_traits( } } +fn generate_surface_lowering( + module: &Ident, + instruction_ident: &Ident, + generics: &Generics, + syntax: &[SyntaxDeclaration], + routes: &[RouteDeclaration], + error: Option<&Type>, +) -> TokenStream2 { + if syntax.is_empty() || routes.is_empty() || error.is_none() { + return quote! {}; + } + let error = error.expect("checked above"); + let route_generics = retained_enum_generics(generics, routes); + let (_, route_ty_generics, _) = route_generics.split_for_impl(); + let arms = syntax.iter().filter_map(|entry| { + let variant = &entry.variant; + let route = match &entry.mapping { + SyntaxMapping::Runtime(runtime_variant) => routes + .iter() + .find(|route| route.variant == *runtime_variant) + .expect("validated runtime route"), + SyntaxMapping::Lower(_) => return None, + }; + let runtime_variant = &route.variant; + let payload = &route.payload; + Some(quote! { + #module::syntax::Instruction::#variant => { + Ok(vec![#instruction_ident::#runtime_variant(#payload)]) + } + }) + }); + let lowerer_arms = syntax.iter().filter_map(|entry| { + let SyntaxMapping::Lower(method) = &entry.mapping else { + return None; + }; + let variant = &entry.variant; + Some(quote! { + #module::syntax::Instruction::#variant(instruction) => { + ::#method(self, instruction) + .map_err(::std::convert::Into::<#error>::into) + } + }) + }); + quote! { + fn lower_surface_instruction( + &mut self, + instruction: &#module::syntax::Instruction, + ) -> ::std::result::Result< + ::std::vec::Vec<#instruction_ident #route_ty_generics>, + #error, + > { + match instruction.clone() { + #( #arms, )* + #( #lowerer_arms, )* + } + } + } +} + +#[allow(clippy::too_many_arguments)] +fn generate_program_loading( + root: &TokenStream2, + name: &Ident, + module: &Ident, + generics: &Generics, + error: Option<&Type>, + syntax: &[SyntaxDeclaration], + routes: &[RouteDeclaration], + fields: &[super::validate::FieldMetadata], +) -> TokenStream2 { + let Some(error) = error else { + return quote! {}; + }; + let Some(program) = fields.iter().find(|field| field.program) else { + return quote! {}; + }; + if syntax.is_empty() || routes.is_empty() { + return quote! {}; + } + + let program_field = &program.ident; + let program_ty = &program.ty; + let instruction_ident = format_ident!("{name}Instruction"); + let route_generics = retained_enum_generics(generics, routes); + let (_, route_ty_generics, _) = route_generics.split_for_impl(); + let syntax_generics = syntax_generics(generics, syntax); + let (_, syntax_ty_generics, _) = syntax_generics.split_for_impl(); + let surface_ty = format_ident!("__VihacoSurfaceType"); + let header_ty = format_ident!("__VihacoHeader"); + let context_ty = format_ident!("__VihacoContext"); + + quote! { + pub fn resolve_parsed<#surface_ty, #header_ty>( + &mut self, + parsed: #root::syntax::ParsedModule< + #module::syntax::Instruction #syntax_ty_generics, + #surface_ty, + #header_ty, + >, + ) -> ::eyre::Result< + <#program_ty as #root::BuildProgramModule>::Module, + > + where + #surface_ty: ::std::clone::Clone + + ::std::convert::Into< + <#program_ty as #root::BuildProgramModule>::Type, + >, + #program_ty: #root::BuildProgramModule< + Instruction = #instruction_ident #route_ty_generics, + >, + { + let mut module = <#program_ty as #root::BuildProgramModule>::empty_module(); + for (function_index, function) in parsed.functions.into_iter().enumerate() { + let start_address = + <#program_ty as #root::BuildProgramModule>::instruction_count(&module); + for instruction in function.body { + let lowered = self.lower_surface_instruction(&instruction)?; + <#program_ty as #root::BuildProgramModule>::append_instructions( + &mut module, + lowered, + ); + } + let end_address = + <#program_ty as #root::BuildProgramModule>::instruction_count(&module); + let function_name = + <#program_ty as #root::BuildProgramModule>::intern_string( + &mut module, + function.name.as_str().to_owned(), + ); + let params = function + .params + .into_iter() + .map(|param| { + let name = <#program_ty as #root::BuildProgramModule>::intern_string( + &mut module, + param.name.as_str().to_owned(), + ); + #root::module::Parameter { + name, + ty: param.ty.into(), + } + }) + .collect(); + let ret = function + .return_ty + .into_iter() + .map(::std::convert::Into::into) + .collect(); + <#program_ty as #root::BuildProgramModule>::add_function( + &mut module, + #root::module::FunctionInfo { + name: function_name, + signature: #root::module::Signature { params, ret }, + local_count: 0, + start_address, + end_address, + file: 0, + }, + ); + if function.name.as_str() == "main" { + <#program_ty as #root::BuildProgramModule>::set_main_function( + &mut module, + Some(function_index as u32), + ); + } + } + <#program_ty as #root::BuildProgramModule>::finish(module) + .map_err(::std::convert::Into::<#error>::into) + } + + pub fn load_parsed<#surface_ty, #header_ty, #context_ty>( + &mut self, + parsed: #root::syntax::ParsedModule< + #module::syntax::Instruction #syntax_ty_generics, + #surface_ty, + #header_ty, + >, + context: #root::ContextHandle<#context_ty>, + ) -> ::eyre::Result<()> + where + #surface_ty: ::std::clone::Clone + + ::std::convert::Into< + <#program_ty as #root::BuildProgramModule>::Type, + >, + #program_ty: #root::BuildProgramModule< + Instruction = #instruction_ident #route_ty_generics, + > + #root::InstallProgramModule< + #context_ty, + Module = <#program_ty as #root::BuildProgramModule>::Module, + >, + { + let module = self.resolve_parsed(parsed)?; + <#program_ty as #root::InstallProgramModule<#context_ty>> + ::install_program_module(&mut self.#program_field, module, context) + .map_err(::std::convert::Into::<#error>::into) + } + } +} + pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result { let root = resolve_root(&declaration.attrs)?; let fields_metadata = super::validate::metadata_fields(&declaration.fields)?; @@ -189,7 +388,30 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result Result Result, pub(super) loadable: Option, + pub(super) program: bool, } fn validate_loadable_name(name: &str, span: Span) -> Result<()> { @@ -42,6 +43,7 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { .ok_or_else(|| syn::Error::new(field.span(), "composite fields must be named"))?; let mut device = None; let mut loadable = None; + let mut program = false; for attr in &field.attrs { if attr.path().is_ident("device") { if device.is_some() { @@ -65,6 +67,14 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { }; validate_loadable_name(&name, attr.span())?; loadable = Some(name); + } else if attr.path().is_ident("program") { + if program { + return Err(syn::Error::new( + attr.span(), + format!("duplicate program attribute on field `{ident}`"), + )); + } + program = true; } } if loadable.is_some() && device.is_none() { @@ -78,13 +88,26 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { ty: field.ty.clone(), device, loadable, + program, }); } let mut device_codes = BTreeMap::::new(); let mut source_symbols = BTreeMap::::new(); let mut loadable_names = BTreeMap::::new(); + let mut program_field = None; for field in &metadata { + if field.program + && let Some(previous) = program_field.replace(field.ident.clone()) + { + return Err(syn::Error::new( + field.ident.span(), + format!( + "multiple `#[program]` fields: `{previous}` and `{}`", + field.ident + ), + )); + } let Some(device) = &field.device else { continue; }; diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index 8ab5b86e..b93eb590 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -36,8 +36,8 @@ pub use instruction_syntax::{ InstructionSugarVariantSyntax, OperandKind, SugarOperandKind, }; pub use loader::{ - InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, - LoadSstSection, ProgramImage, + BuildProgramModule, InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, + LoadOwnSstSection, LoadSstSection, ProgramImage, }; pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index 21d0b474..54974744 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -3,21 +3,20 @@ use chumsky::Parser as _; use eyre::Result; -use vihaco::{Effects, Execute, Execution, Instruction, Observe, StepResult, composite}; -use vihaco_parser::Parse; +use vihaco::{Effects, Execute, Execution, Observe, StepResult, composite}; +use vihaco_parser::{Ident, Parse}; mod test_root { pub use ::vihaco::*; } -#[derive(Debug, Clone, Instruction)] -pub enum TestInstruction { - Run, -} +#[derive(Debug, Clone, Copy)] +pub struct TestInstruction; struct TestMessage; struct TestEffect; struct TestComponent; +struct TestContext; impl Execute for TestComponent { type Message = TestMessage; @@ -59,6 +58,8 @@ composite! { #[device(0x01)] component: TestComponent, observer: TestObserver, + #[program] + program: vihaco::ProgramImage, } syntax { @@ -89,6 +90,15 @@ impl TestMachine { } } +impl test_machine::syntax::Resolver for TestMachine { + fn lower_count( + &mut self, + _instruction: u32, + ) -> Result, eyre::Report> { + Ok(Vec::new()) + } +} + #[test] fn runtime_macros_honor_explicit_crate_override() { let parsed = test_machine::syntax::Instruction::parser() @@ -100,9 +110,10 @@ fn runtime_macros_honor_explicit_crate_override() { let mut machine = TestMachine { component: TestComponent, observer: TestObserver::default(), + program: vihaco::ProgramImage::new(), }; let outcome = machine - .execute_generated(&TestMachineInstruction::Run(TestInstruction::Run)) + .execute_generated(&TestMachineInstruction::Run(TestInstruction)) .unwrap(); assert_eq!(outcome, Execution::Complete); assert!(machine.observer.observed); @@ -111,3 +122,30 @@ fn runtime_macros_honor_explicit_crate_override() { assert_eq!(metadata.devices[0].code, 0x01); assert_eq!(metadata.devices[0].name, "component"); } + +#[test] +fn generated_program_loader_builds_and_installs_module() { + let parsed = vihaco::syntax::ParsedModule { + header: (), + functions: vec![vihaco::syntax::ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::>::new(), + return_ty: None, + body: vec![test_machine::syntax::Instruction::Run], + }], + }; + let mut machine = TestMachine { + component: TestComponent, + observer: TestObserver::default(), + program: vihaco::ProgramImage::new(), + }; + + machine + .load_parsed(parsed, vihaco::ContextHandle::new(TestContext)) + .unwrap(); + + assert_eq!(machine.program.module.code.len(), 1); + assert_eq!(machine.program.module.functions.len(), 1); + assert_eq!(machine.program.module.main_function, Some(0)); + assert_eq!(machine.program.pc, 0); +} diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index fe61db5f..d52ac7d1 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -125,6 +125,77 @@ The composite macro can also declare structural composites with no metadata, and section wiring, while their event loop or parent dispatch remains hand-written. +## Surface syntax and program loading + +An executable composite can own the source grammar for its machine program. +The `syntax` block declares surface instructions, while the `runtime` block +declares the runtime routes they lower to: + +```rust ignore +composite Machine { + error = eyre::Report; + + #[device(0x01)] + cpu: Cpu, + + #[program] + program: ProgramImage, +} + +syntax { + #[pattern = "'machine::halt"] + Halt => runtime Halt; + #[pattern = "'machine::load $0"] + Load(u64) => lower_load; +} + +runtime { + Halt(Halt) => cpu { message none; } + LoadConstant(u64) => cpu { message none; } +} +``` + +Direct `runtime` mappings are intended for unit surface instructions. A named +lowerer handles payloads and may expand one surface instruction into several +runtime instructions: + +```rust ignore +impl machine::syntax::Resolver for Machine { + fn lower_load( + &mut self, + value: u64, + ) -> Result> { + Ok(vec![machine::runtime::Instruction::LoadConstant(value)]) + } +} +``` + +The generated parser is available as +`machine::syntax::Instruction::parser()`. A parsed module can be resolved and +installed with an explicit context: + +```rust ignore +let parsed = machine::syntax::ParsedModule::parse_section(section)?; +machine.load_parsed(parsed, ContextHandle::new(MachineContext))?; +``` + +`load_parsed` constructs a fresh module, lowers every function, records +function metadata, selects `main`, installs the module and context, and resets +the program counter. Malformed input or a lowering failure returns an error. + +## Custom program containers + +`ProgramImage` is the standard program container. A composite author only +needs to mark its program field with `#[program]`. A library author who needs +custom storage or metadata can implement `BuildProgramModule` and +`InstallProgramModule` for another container. The builder controls module +creation, instruction appending, string interning, function metadata, +constants, and final validation; generated `load_parsed` uses those operations +without depending on `LocalModule` directly. + +This keeps source resolution independent from the representation used by a +particular host VM. + ## Runtime boundaries The macro does not fetch instructions, own a program counter, generate a clock, diff --git a/docs/src/pages/guide/parser-advanced.md b/docs/src/pages/guide/parser-advanced.md index c6f34d5d..aa22f5ca 100644 --- a/docs/src/pages/guide/parser-advanced.md +++ b/docs/src/pages/guide/parser-advanced.md @@ -205,3 +205,16 @@ and load the resulting runtime instructions into that component. This keeps source syntax attached to the component that owns it. Composite loading routes sections to components; it does not require a second source grammar for the generated machine instruction enum. + +For an executable composite with a `#[program]` field, the composite can own +this final step instead of requiring a separate handwritten `Resolve` +implementation. Its generated `syntax::Instruction` is the surface type, and +its generated resolver lowers those values into the composite's runtime +instruction enum. Call `load_parsed(parsed, context)` to build and install the +program. Use `resolve_parsed(parsed)` when the constructed module needs to be +inspected before installation. + +The program field's container supplies the construction policy through +`BuildProgramModule`. `ProgramImage` implements the standard policy for +`LocalModule`; custom containers can implement the same capability when they +need different storage or metadata. diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md index 1072fa65..71c5052f 100644 --- a/vision/execution-pipeline.md +++ b/vision/execution-pipeline.md @@ -17,6 +17,27 @@ SST text -> runtime program image ``` +An executable `composite!` can now own the surface-to-runtime boundary. Its +`syntax` block generates the surface instruction parser, its named resolver +methods lower payload-bearing instructions, and a `#[program]` field receives +the resulting module through `BuildProgramModule` and +`InstallProgramModule`: + +```text +SST section + -> composite::syntax::Instruction parser + -> ParsedModule + -> generated composite lowering + -> BuildProgramModule + -> load_parsed(parsed, ContextHandle) + -> installed runtime program +``` + +The generic `Resolve` trait remains available for standalone or more +application-specific module construction. The generated composite path is a +convenience for executable composites and does not require the runtime +instruction enum to implement bytecode encoding. + `SurfaceType`, `Constant`, and `RuntimeType` are author-defined products rather than vihaco enums. `Resolve` owns every transformation that requires module-wide source context: From 3dcc23a42f88b93741892977006f8abe47650476 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Mon, 10 Aug 2026 13:34:06 -0400 Subject: [PATCH 09/15] Migrated support for loading SST sections to rewrite, and removed bytecode loading (keeping the bytecode container for planned codegen) --- crates/vihaco-module/src/loader.rs | 26 +- .../design/composite_macro.md | 2 +- .../src/composite/codegen.rs | 45 + .../src/composite/loadable.rs | 79 +- crates/vihaco/src/lib.rs | 32 +- crates/vihaco/tests/generated_sst_loading.rs | 211 +++++ crates/vihaco/tests/multisection_bytecode.rs | 825 ------------------ .../tests/runtime_macro_crate_override.rs | 57 +- docs/src/pages/guide/composites.md | 11 +- 9 files changed, 330 insertions(+), 958 deletions(-) create mode 100644 crates/vihaco/tests/generated_sst_loading.rs delete mode 100644 crates/vihaco/tests/multisection_bytecode.rs diff --git a/crates/vihaco-module/src/loader.rs b/crates/vihaco-module/src/loader.rs index 658acf9a..0d232542 100644 --- a/crates/vihaco-module/src/loader.rs +++ b/crates/vihaco-module/src/loader.rs @@ -2,23 +2,11 @@ // SPDX-License-Identifier: MIT use vihaco_abi::program::{Type, Value}; -use vihaco_bytecode::{BytecodeSectionView, ConstantId, ContextHandle, SstSectionView}; +use vihaco_bytecode::{ConstantId, ContextHandle, SstSectionView}; use crate::host::{GetProgramInfo, ProgramCounter}; use crate::module::{FunctionInfo, LabelInfo, LocalModule, NoInfo, SourceSymbolInfo}; -/// Allow a machine to load the bytecode data owned directly by its section. -/// -/// When used with the [`vihaco_runtime_derive::composite`] macro, this hook runs before -/// generated child-section forwarding. Implement this for each composite to -/// make its own section behavior explicit, even when that behavior is a no-op. -pub trait LoadOwnBytecodeSection { - fn load_own_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, C>, - ) -> eyre::Result<()>; -} - /// Allow a machine to load the SST data owned directly by its section. /// /// When used with the [`vihaco_runtime_derive::composite`] macro, this hook runs before @@ -28,18 +16,6 @@ pub trait LoadOwnSstSection { fn load_own_sst_section<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; } -/// Allow a machine to load a bytecode section completely. -/// -/// For composites generated by [`vihaco_runtime_derive::composite`], this loads the -/// composite's own section through [`LoadOwnBytecodeSection`] and then forwards -/// direct child sections to `#[loadable]` devices. -pub trait LoadBytecodeSection { - fn load_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, C>, - ) -> eyre::Result<()>; -} - /// Allow a machine to load an SST section completely. /// /// For composites generated by [`vihaco_runtime_derive::composite`], this loads the diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md index 98d50040..3e811134 100644 --- a/crates/vihaco-runtime-derive/design/composite_macro.md +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -185,7 +185,7 @@ cpu: Cpu, fpga: Fpga, ``` -`#[loadable]` continues to identify device fields that participate in generated bytecode/SST +`#[loadable]` identifies device fields that participate in generated SST section loading. `#[program]` marks the program container used by generated parsed-module loading. The container must implement `BuildProgramModule` and `InstallProgramModule`; `ProgramImage` supplies the standard implementation. diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index c0de113d..941282ec 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -235,6 +235,13 @@ fn generate_program_loading( let surface_ty = format_ident!("__VihacoSurfaceType"); let header_ty = format_ident!("__VihacoHeader"); let context_ty = format_ident!("__VihacoContext"); + let loadable_predicates = fields + .iter() + .filter(|field| field.loadable.is_some()) + .map(|field| { + let field_ty = &field.ty; + quote! { #field_ty: #root::loader::LoadSstSection<#context_ty> } + }); quote! { pub fn resolve_parsed<#surface_ty, #header_ty>( @@ -341,6 +348,44 @@ fn generate_program_loading( ::install_program_module(&mut self.#program_field, module, context) .map_err(::std::convert::Into::<#error>::into) } + + pub fn load_source<'__vihaco_sst, #surface_ty, #header_ty, #context_ty>( + &mut self, + section: #root::SstSectionView<'__vihaco_sst, #context_ty>, + ) -> ::eyre::Result<()> + where + #module::syntax::Instruction #syntax_ty_generics: + #root::Parse<'__vihaco_sst> + '__vihaco_sst, + #surface_ty: #root::Parse<'__vihaco_sst> + + '__vihaco_sst + + ::std::clone::Clone + + ::std::convert::Into< + <#program_ty as #root::BuildProgramModule>::Type, + >, + #header_ty: #root::SstHeader, + #program_ty: #root::BuildProgramModule< + Instruction = #instruction_ident #route_ty_generics, + > + #root::InstallProgramModule< + #context_ty, + Module = <#program_ty as #root::BuildProgramModule>::Module, + >, + #( #loadable_predicates ),* + { + let parsed = #root::syntax::ParsedModule::< + #module::syntax::Instruction #syntax_ty_generics, + #surface_ty, + #header_ty, + >::parse_section(section.clone())?; + let module = self.resolve_parsed(parsed)?; + <#program_ty as #root::InstallProgramModule<#context_ty>> + ::install_program_module( + &mut self.#program_field, + module, + section.context_handle(), + ) + .map_err(::std::convert::Into::<#error>::into)?; + self.load_generated_sst_children(section) + } } } diff --git a/crates/vihaco-runtime-derive/src/composite/loadable.rs b/crates/vihaco-runtime-derive/src/composite/loadable.rs index f3625ae1..d0b4feef 100644 --- a/crates/vihaco-runtime-derive/src/composite/loadable.rs +++ b/crates/vihaco-runtime-derive/src/composite/loadable.rs @@ -20,19 +20,9 @@ pub(super) fn generate_loadable_impls( let context = format_ident!("__VihacoContext"); let (_, ty_generics, _) = generics.split_for_impl(); - let own_bytecode_predicate = quote! { - #name #ty_generics: #root::loader::LoadOwnBytecodeSection<#context> - }; let own_sst_predicate = quote! { #name #ty_generics: #root::loader::LoadOwnSstSection<#context> }; - let bytecode_method_predicates: Vec<_> = loadables - .iter() - .map(|field| { - let field_ty = &field.ty; - quote! { #field_ty: #root::loader::LoadBytecodeSection<#context> } - }) - .collect(); let sst_method_predicates: Vec<_> = loadables .iter() .map(|field| { @@ -40,20 +30,6 @@ pub(super) fn generate_loadable_impls( quote! { #field_ty: #root::loader::LoadSstSection<#context> } }) .collect(); - let bytecode_children: Vec<_> = loadables - .iter() - .map(|field| { - let field_ident = &field.ident; - let field_ty = &field.ty; - let section_name = field.loadable.as_ref().expect("loadable field"); - quote! { - if let ::std::option::Option::Some(child) = section.child(#section_name) { - <#field_ty as #root::loader::LoadBytecodeSection<#context>> - ::load_bytecode_section(&mut self.#field_ident, child)?; - } - } - }) - .collect(); let sst_children: Vec<_> = loadables .iter() .map(|field| { @@ -91,28 +67,6 @@ pub(super) fn generate_loadable_impls( } }; - let mut bytecode_impl_generics = generics.clone(); - bytecode_impl_generics - .params - .push(syn::parse_quote!(#context)); - { - let where_clause = bytecode_impl_generics.make_where_clause(); - where_clause - .predicates - .push(syn::parse2(own_bytecode_predicate.clone()).expect("valid predicate")); - for field in &loadables { - let field_ty = &field.ty; - where_clause.predicates.push( - syn::parse2(quote! { - #field_ty: #root::loader::LoadBytecodeSection<#context> - }) - .expect("valid predicate"), - ); - } - } - let (bytecode_impl_generics, _, bytecode_where_clause) = - bytecode_impl_generics.split_for_impl(); - let mut sst_impl_generics = generics.clone(); sst_impl_generics.params.push(syn::parse_quote!(#context)); { @@ -135,53 +89,34 @@ pub(super) fn generate_loadable_impls( quote! { impl #impl_generics #name #ty_generics #where_clause { - pub fn load_generated_bytecode_sections<'__vihaco_bc, #context>( + pub fn load_generated_sst_sections<'__vihaco_sst, #context>( &mut self, - section: #root::BytecodeSectionView<'__vihaco_bc, #context>, + section: #root::SstSectionView<'__vihaco_sst, #context>, ) -> ::eyre::Result<()> where - #name #ty_generics: #root::loader::LoadOwnBytecodeSection<#context>, - #( #bytecode_method_predicates ),* + #name #ty_generics: #root::loader::LoadOwnSstSection<#context>, + #( #sst_method_predicates ),* { - #root::loader::LoadOwnBytecodeSection::<#context>::load_own_bytecode_section( + #root::loader::LoadOwnSstSection::<#context>::load_own_sst_section( self, section.clone(), )?; - #expected_children - #( #bytecode_children )* - Ok(()) + self.load_generated_sst_children(section) } - pub fn load_generated_sst_sections<'__vihaco_sst, #context>( + pub fn load_generated_sst_children<'__vihaco_sst, #context>( &mut self, section: #root::SstSectionView<'__vihaco_sst, #context>, ) -> ::eyre::Result<()> where - #name #ty_generics: #root::loader::LoadOwnSstSection<#context>, #( #sst_method_predicates ),* { - #root::loader::LoadOwnSstSection::<#context>::load_own_sst_section( - self, - section.clone(), - )?; #expected_children #( #sst_children )* Ok(()) } } - impl #bytecode_impl_generics #root::loader::LoadBytecodeSection<#context> - for #name #ty_generics - #bytecode_where_clause - { - fn load_bytecode_section<'__vihaco_bc>( - &mut self, - section: #root::BytecodeSectionView<'__vihaco_bc, #context>, - ) -> ::eyre::Result<()> { - self.load_generated_bytecode_sections(section) - } - } - impl #sst_impl_generics #root::loader::LoadSstSection<#context> for #name #ty_generics #sst_where_clause diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index b93eb590..399a1c49 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -36,8 +36,7 @@ pub use instruction_syntax::{ InstructionSugarVariantSyntax, OperandKind, SugarOperandKind, }; pub use loader::{ - BuildProgramModule, InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, - LoadOwnSstSection, LoadSstSection, ProgramImage, + BuildProgramModule, InstallProgramModule, LoadOwnSstSection, LoadSstSection, ProgramImage, }; pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; @@ -47,16 +46,15 @@ pub use runtime::{ expect_exactly_one_effect, }; pub use traits::{FromBytes, FromText, GetProgramInfo, Reset}; -pub use vihaco_parser::SurfaceInstruction; +pub use vihaco_parser::{Parse, SurfaceInstruction}; pub use vihaco_parser_derive::Parse; #[cfg(test)] mod public_api_tests { use crate::{ BytecodeGlobalContext, BytecodeHeader, ConstantId, EffectSink, Effects, Execute, Execution, - GlobalContext, InstallProgramModule, LoadBytecodeSection, LoadOwnBytecodeSection, - ProgramImage, Reset, SectionNameResolver, SstGlobalContext, SstHeader, StepResult, - WriteBytecodeHeader, + GlobalContext, InstallProgramModule, ProgramImage, Reset, SectionNameResolver, + SstGlobalContext, SstHeader, StepResult, WriteBytecodeHeader, instruction::{FromBytes, OpCode, WriteBytes}, module::FunctionInfo, observer::stdio::StdoutEffect, @@ -88,24 +86,6 @@ mod public_api_tests { } } - impl LoadOwnBytecodeSection for PublicReset { - fn load_own_bytecode_section<'bc>( - &mut self, - _section: crate::BytecodeSectionView<'bc, PublicContext>, - ) -> eyre::Result<()> { - Ok(()) - } - } - - impl LoadBytecodeSection for PublicReset { - fn load_bytecode_section<'bc>( - &mut self, - section: crate::BytecodeSectionView<'bc, PublicContext>, - ) -> eyre::Result<()> { - self.load_own_bytecode_section(section) - } - } - struct PublicSstHeader; impl crate::traits::FromText for PublicSstHeader { @@ -128,8 +108,6 @@ mod public_api_tests { fn require_bytecode_global_context() {} fn require_sst_global_context() {} fn require_global_context() {} - fn require_load_own_bytecode_section>() {} - fn require_load_bytecode_section>() {} fn require_install_program_module>() {} fn require_stdout_effect(_effect: StdoutEffect) {} fn require_metadata(_metadata: crate::CompositeMetadata) {} @@ -145,8 +123,6 @@ mod public_api_tests { require_sst_global_context::(); require_sst_global_context::(); require_global_context::(); - require_load_own_bytecode_section::(); - require_load_bytecode_section::(); require_install_program_module::>(); let _constant = ConstantId(0); let _function: Option> = None; diff --git a/crates/vihaco/tests/generated_sst_loading.rs b/crates/vihaco/tests/generated_sst_loading.rs new file mode 100644 index 00000000..3b844f7f --- /dev/null +++ b/crates/vihaco/tests/generated_sst_loading.rs @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use eyre::Result; +use vihaco::{ + ContextHandle, Effects, Execute, Execution, LoadOwnSstSection, LoadSstSection, NoEffect, + NoMessage, ProgramImage, SstFile, SstGlobalContext, SstHeader, SstSectionView, StepResult, + Type, Value, + syntax::{Param, ParsedFunction, ParsedModule}, + traits::FromText, +}; +use vihaco_parser::Ident; +use vihaco_parser_derive::Parse; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Parse)] +#[syntax_class(type)] +enum ParsedType { + #[pattern = "`i64`"] + I64, +} + +impl From for Type { + fn from(_value: ParsedType) -> Self { + Self::I64 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RuntimeInstruction; + +#[derive(Debug, Default)] +struct TestComponent; + +impl Execute for TestComponent { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &RuntimeInstruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TestContext { + name: String, +} + +impl SstGlobalContext for TestContext { + fn from_text(text: &str) -> Result { + Ok(Self { + name: text.trim().to_owned(), + }) + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct NoHeader; + +impl SstHeader for NoHeader {} + +impl FromText for NoHeader { + fn from_text(_text: &str) -> Result { + Ok(Self) + } +} + +#[derive(Debug, Default)] +struct ChildLoader { + loaded_sst: Option, + context: Option>, +} + +impl LoadSstSection for ChildLoader { + fn load_sst_section<'src>(&mut self, section: SstSectionView<'src, TestContext>) -> Result<()> { + self.loaded_sst = Some(section.sst().to_owned()); + self.context = Some(section.context_handle()); + Ok(()) + } +} + +vihaco::composite! { + #[derive(Default)] + #[allow(dead_code)] + composite TestMachine { + error = eyre::Report; + + #[device(0x01)] + component: TestComponent, + + #[program] + program: ProgramImage, + + #[device(0x02)] + #[loadable("child")] + child: ChildLoader, + } + + syntax { + #[pattern = "'test::run"] + Run => runtime Run; + } + + runtime { + Run(RuntimeInstruction) => component { + message none; + } + } +} + +impl LoadOwnSstSection for TestMachine { + fn load_own_sst_section<'src>( + &mut self, + section: SstSectionView<'src, TestContext>, + ) -> Result<()> { + let context = section.context_handle(); + let parsed = ParsedModule { + header: NoHeader, + functions: vec![ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::>::new(), + return_ty: None, + body: vec![test_machine::syntax::Instruction::Run], + }], + }; + self.load_parsed(parsed, context) + } +} + +fn root_file(source: &str) -> SstFile { + SstFile::from_text(&format!( + "sst v1\n\n.global:\nroot-context\n.global.\n\n{source}" + )) + .expect("test SST should parse") +} + +#[test] +fn generated_sst_root_loads_program_and_forwards_children() { + let file = root_file( + ".section(root):\n\ +\t.text(root):\n\ +\t\tfn @main() {\n\ +\t\t\ttest::run\n\ +\t\t}\n\ +\t.text(root).\n\ +\t.section(child):\n\ +\t\t.text(child):\n\ +\t\t\tchild payload\n\ +\t\t.text(child).\n\ +\t.section(child).\n\ +.section(root).\n", + ); + let context = file.context_handle(); + let mut machine = TestMachine::default(); + + machine.load_sst_section(file.root()).unwrap(); + + assert_eq!(machine.program.module.code.len(), 1); + assert_eq!(machine.program.module.functions.len(), 1); + assert_eq!(machine.program.module.main_function, Some(0)); + assert_eq!(machine.program.pc, 0); + assert!(machine.program.context.is_some()); + assert!( + machine + .child + .loaded_sst + .as_deref() + .is_some_and(|sst| sst.contains("child payload")) + ); + assert!( + machine + .child + .context + .as_ref() + .is_some_and(|loaded| loaded.ptr_eq(&context)) + ); +} + +#[test] +fn malformed_root_source_is_rejected_before_program_installation() { + let file = root_file( + ".section(root):\n\ +\t.text(root):\n\ +\t\tfn @main() {\n\ +\t\t\ttest::does_not_exist\n\ +\t\t}\n\ +\t.text(root).\n\ +.section(root).\n", + ); + let result = + ParsedModule::::parse_section( + file.root(), + ); + let error = match result { + Ok(_) => panic!("malformed source unexpectedly parsed"), + Err(error) => error, + }; + + assert!(!error.to_string().is_empty()); + let machine = TestMachine::default(); + assert!(machine.program.module.code.is_empty()); + assert!(machine.program.context.is_none()); + assert_eq!(machine.child.loaded_sst, None); +} diff --git a/crates/vihaco/tests/multisection_bytecode.rs b/crates/vihaco/tests/multisection_bytecode.rs deleted file mode 100644 index f1241129..00000000 --- a/crates/vihaco/tests/multisection_bytecode.rs +++ /dev/null @@ -1,825 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use std::{io::Read, str::FromStr}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use vihaco::{ - BytecodeFile, BytecodeGlobalContext, BytecodeSectionView, ConstantId, FLAGS, GetProgramInfo, - Instruction, LoadBytecodeSection, LoadOwnBytecodeSection, LoadOwnSstSection, LoadSstSection, - MAGIC, ProgramImage, SectionNameResolver, SstFile, SstGlobalContext, SstHeader, SstSectionView, - Type, VERSION, Value, - module::LocalModule, - syntax::{ParsedModule, Resolve}, - traits::{FromBytes, FromText, WriteBytes}, -}; - -const CHILD_NAME: u32 = 0; -const DEFAULT_CHILD_NAME: u32 = 1; -const EXTRA_NAME: u32 = 2; -const MIDDLE_NAME: u32 = 3; -const LEAF_NAME: u32 = 4; -const SECTION_FRAME_LEN: usize = 8 + 8; -const SECTION_BYTECODE_HEADER_LEN: usize = 8; -const CHILD_SECTION_TABLE_HEADER_LEN: usize = 4; -const CHILD_SECTION_TABLE_ENTRY_LEN: usize = 4 + 8; - -#[derive(Debug, Clone, PartialEq, Instruction)] -enum TestInst { - Nop, - Load(ConstantId), -} - -#[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser_derive::Parse)] -#[syntax_class(instruction)] -enum TextInst { - #[pattern = "'test::nop"] - Nop, - #[pattern = "'test::alt"] - Alt, -} - -#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] -#[syntax_class(instruction)] -enum SurfaceOnlyInst { - #[pattern = "'surface::nop"] - Nop, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, vihaco_parser_derive::Parse)] -#[syntax_class(type)] -enum TextType { - #[pattern = "`i64`"] - I64, -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -struct TestHeader { - cores: u32, -} - -impl FromBytes for TestHeader { - fn from_bytes(bytes: &mut R) -> eyre::Result { - Ok(Self { - cores: bytes.read_u32::()?, - }) - } -} - -impl FromText for TestHeader { - fn from_text(text: &str) -> eyre::Result { - Ok(text.trim().parse()?) - } -} - -impl SstHeader for TestHeader {} - -impl WriteBytes for TestHeader { - fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { - io.write_u32::(self.cores)?; - Ok(()) - } -} - -impl FromStr for TestHeader { - type Err = std::num::ParseIntError; - - fn from_str(text: &str) -> Result { - Ok(Self { - cores: text.trim().parse()?, - }) - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -struct NoHeader; - -impl FromText for NoHeader { - fn from_text(_text: &str) -> eyre::Result { - Ok(Self) - } -} - -impl SstHeader for NoHeader {} - -#[derive(Debug, Default)] -struct TextResolver; - -impl Resolve for TextResolver { - type Module = LocalModule; - - fn resolve_module( - &mut self, - parsed: ParsedModule, - ) -> eyre::Result { - let mut module = LocalModule::default(); - for function in parsed.functions { - module.code.extend(function.body); - } - Ok(module) - } -} - -type BytecodeProgram = ProgramImage; -type TextProgram = ProgramImage; - -fn load_bytecode_program<'bc>( - program: &mut BytecodeProgram, - section: BytecodeSectionView<'bc, TextContext>, -) -> eyre::Result<()> { - program.module.code = section.decode_instructions()?; - program.module.constants = vec![Value::I64(9)]; - program.context = Some(section.context_handle()); - program.pc = 0; - Ok(()) -} - -fn load_parsed_text_program( - program: &mut TextProgram, - parsed: ParsedModule, - context: vihaco::ContextHandle, -) -> eyre::Result<()> { - let mut resolver = TextResolver; - program.module = resolver.resolve_module(parsed)?; - program.context = Some(context); - program.pc = 0; - Ok(()) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct TextContext { - section_names: Vec, -} - -impl SectionNameResolver for TextContext { - fn section_name(&self, index: u32) -> Option<&str> { - self.section_names.get(index as usize).map(String::as_str) - } -} - -impl BytecodeGlobalContext for TextContext { - fn from_bytes(bytes: &[u8]) -> eyre::Result { - let text = std::str::from_utf8(bytes)?; - ::from_text(text) - } -} - -impl SstGlobalContext for TextContext { - fn from_text(text: &str) -> eyre::Result { - Ok(Self { - section_names: text - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(ToOwned::to_owned) - .collect(), - }) - } -} - -#[derive(Debug, Clone, Default)] -struct LoadedDevice { - program: BytecodeProgram, -} - -#[derive(Debug, Clone, Default)] -struct TextLoadedDevice { - program: TextProgram, -} - -impl LoadBytecodeSection for LoadedDevice { - fn load_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - load_bytecode_program(&mut self.program, section) - } -} - -impl LoadSstSection for TextLoadedDevice { - fn load_sst_section<'bc>( - &mut self, - section: SstSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - let parsed = ParsedModule::::parse_section(section.clone())?; - load_parsed_text_program(&mut self.program, parsed, section.context_handle())?; - Ok(()) - } -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite Machine { - program: BytecodeProgram, - - #[device(0x01)] - #[loadable("child")] - child: LoadedDevice, - - #[device(0x02)] - #[loadable] - default_child: LoadedDevice, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite NestedMachine { - program: BytecodeProgram, - - #[device(0x01)] - #[loadable("leaf")] - leaf: LoadedDevice, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite HostMachine { - program: BytecodeProgram, - - #[device(0x01)] - #[loadable("middle")] - middle: NestedMachine, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite HeaderMachine { - info: TestHeader, - - program: BytecodeProgram, - - #[device(0x01)] - device: LoadedDevice, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite TextMachine { - program: TextProgram, - - #[device(0x01)] - #[loadable("child")] - child: TextLoadedDevice, - - #[device(0x02)] - #[loadable] - default_child: TextLoadedDevice, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite TextNestedMachine { - program: TextProgram, - - #[device(0x01)] - #[loadable("leaf")] - leaf: TextLoadedDevice, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite TextHostMachine { - program: TextProgram, - - #[device(0x01)] - #[loadable("middle")] - middle: TextNestedMachine, -} -} - -vihaco::composite! { -#[derive(Debug, Default)] -#[allow(dead_code)] -composite TextHeaderMachine { - info: TestHeader, - - program: TextProgram, - - #[device(0x01)] - device: TextLoadedDevice, -} -} - -impl LoadOwnBytecodeSection for Machine { - fn load_own_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - load_bytecode_program(&mut self.program, section) - } -} - -impl LoadOwnBytecodeSection for NestedMachine { - fn load_own_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - load_bytecode_program(&mut self.program, section) - } -} - -impl LoadOwnBytecodeSection for HostMachine { - fn load_own_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - load_bytecode_program(&mut self.program, section) - } -} - -impl LoadOwnBytecodeSection for HeaderMachine { - fn load_own_bytecode_section<'bc>( - &mut self, - section: BytecodeSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - self.info = section.decode_header()?; - load_bytecode_program(&mut self.program, section) - } -} - -impl LoadOwnSstSection for TextMachine { - fn load_own_sst_section<'bc>( - &mut self, - section: SstSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - let parsed = ParsedModule::::parse_section(section.clone())?; - load_parsed_text_program(&mut self.program, parsed, section.context_handle())?; - Ok(()) - } -} - -impl LoadOwnSstSection for TextNestedMachine { - fn load_own_sst_section<'bc>( - &mut self, - section: SstSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - let parsed = ParsedModule::::parse_section(section.clone())?; - load_parsed_text_program(&mut self.program, parsed, section.context_handle())?; - Ok(()) - } -} - -impl LoadOwnSstSection for TextHostMachine { - fn load_own_sst_section<'bc>( - &mut self, - section: SstSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - let parsed = ParsedModule::::parse_section(section.clone())?; - load_parsed_text_program(&mut self.program, parsed, section.context_handle())?; - Ok(()) - } -} - -impl LoadOwnSstSection for TextHeaderMachine { - fn load_own_sst_section<'bc>( - &mut self, - section: SstSectionView<'bc, TextContext>, - ) -> eyre::Result<()> { - let parsed = - ParsedModule::::parse_section(section.clone())?; - self.info = parsed.header; - load_parsed_text_program(&mut self.program, parsed, section.context_handle())?; - Ok(()) - } -} - -#[test] -fn parses_surface_instruction_without_runtime_bytecode_traits() { - let file = text_file( - &[], - ".section(root):\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\tsurface::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -.section(root).\n", - ); - - let parsed = - ParsedModule::::parse_section(file.root()).unwrap(); - - assert_eq!(parsed.functions.len(), 1); - assert_eq!(parsed.functions[0].body, vec![SurfaceOnlyInst::Nop]); -} - -#[test] -fn binary_generated_loadable_routes_program_and_child_sections() { - let child = binary_section_bytes(b"", &[TestInst::Load(ConstantId(0))], vec![]); - let default_child = binary_section_bytes(b"", &[TestInst::Nop], vec![]); - let root = binary_section_bytes( - b"", - &[TestInst::Nop], - vec![(CHILD_NAME, child), (DEFAULT_CHILD_NAME, default_child)], - ); - let file: BytecodeFile = - BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); - - let mut machine = Machine::default(); - machine.load_bytecode_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TestInst::Nop]); - assert_eq!( - machine.child.program.module.code, - vec![TestInst::Load(ConstantId(0))] - ); - assert_eq!( - machine.default_child.program.module.code, - vec![TestInst::Nop] - ); - assert!( - machine - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .child - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert_eq!( - machine.program.get_constant(ConstantId(0)).unwrap(), - &Value::I64(9) - ); -} - -#[test] -fn text_generated_loadable_routes_program_and_child_sections() { - let file = text_file( - &["child", "default_child"], - ".section(root):\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\ttest::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -\t.section(child):\n\ -\t\t.text(child):\n\ -\t\t\tfn @main() {\n\ -\t\t\t\ttest::alt\n\ -\t\t\t}\n\ -\t\t.text(child).\n\ -\t.section(child).\n\ -\t.section(default_child):\n\ -\t\t.text(default_child):\n\ -\t\t\tfn @main() {\n\ -\t\t\t\ttest::nop\n\ -\t\t\t}\n\ -\t\t.text(default_child).\n\ -\t.section(default_child).\n\ -.section(root).\n", - ); - - let mut machine = TextMachine::default(); - machine.load_sst_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TextInst::Nop]); - assert_eq!(machine.child.program.module.code, vec![TextInst::Alt]); - assert_eq!( - machine.default_child.program.module.code, - vec![TextInst::Nop] - ); - assert!( - machine - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .child - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); -} - -#[test] -fn binary_generated_loadable_parses_marked_header() { - let mut header = Vec::new(); - TestHeader { cores: 8 }.write_bytes(&mut header).unwrap(); - let root = binary_section_bytes(&header, &[TestInst::Nop], vec![]); - let file: BytecodeFile = - BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); - - let mut machine = HeaderMachine::default(); - machine.load_bytecode_section(file.root()).unwrap(); - - assert_eq!(machine.info, TestHeader { cores: 8 }); - assert_eq!(machine.program.module.code, vec![TestInst::Nop]); -} - -#[test] -fn text_generated_loadable_parses_marked_header() { - let file = text_file( - &[], - ".section(root):\n\ -\t.header(root):\n\ -\t\t8\n\ -\t.header(root).\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\ttest::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -.section(root).\n", - ); - - let mut machine = TextHeaderMachine::default(); - machine.load_sst_section(file.root()).unwrap(); - - assert_eq!(machine.info, TestHeader { cores: 8 }); - assert_eq!(machine.program.module.code, vec![TextInst::Nop]); -} - -#[test] -fn binary_generated_loadable_routes_three_level_section_tree() { - let leaf = binary_section_bytes(b"", &[TestInst::Nop, TestInst::Load(ConstantId(0))], vec![]); - let middle = binary_section_bytes( - b"", - &[TestInst::Load(ConstantId(0))], - vec![(LEAF_NAME, leaf)], - ); - let root = binary_section_bytes(b"", &[TestInst::Nop], vec![(MIDDLE_NAME, middle)]); - let file: BytecodeFile = - BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); - - let mut machine = HostMachine::default(); - machine.load_bytecode_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TestInst::Nop]); - assert_eq!( - machine.middle.program.module.code, - vec![TestInst::Load(ConstantId(0))] - ); - assert_eq!( - machine.middle.leaf.program.module.code, - vec![TestInst::Nop, TestInst::Load(ConstantId(0))] - ); - assert!( - machine - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .middle - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .middle - .leaf - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); -} - -#[test] -fn text_generated_loadable_routes_three_level_section_tree() { - let file = text_file( - &["middle", "leaf"], - ".section(root):\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\ttest::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -\t.section(middle):\n\ -\t\t.text(middle):\n\ -\t\t\tfn @main() {\n\ -\t\t\t\ttest::alt\n\ -\t\t\t}\n\ -\t\t.text(middle).\n\ -\t\t.section(leaf):\n\ -\t\t\t.text(leaf):\n\ -\t\t\t\tfn @main() {\n\ -\t\t\t\t\ttest::nop\n\ -\t\t\t\t\ttest::alt\n\ -\t\t\t\t}\n\ -\t\t\t.text(leaf).\n\ -\t\t.section(leaf).\n\ -\t.section(middle).\n\ -.section(root).\n", - ); - - let mut machine = TextHostMachine::default(); - machine.load_sst_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TextInst::Nop]); - assert_eq!(machine.middle.program.module.code, vec![TextInst::Alt]); - assert_eq!( - machine.middle.leaf.program.module.code, - vec![TextInst::Nop, TextInst::Alt] - ); - assert!( - machine - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .middle - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); - assert!( - machine - .middle - .leaf - .program - .context - .as_ref() - .unwrap() - .ptr_eq(&file.context_handle()) - ); -} - -#[test] -fn binary_generated_loadable_allows_missing_marked_children() { - let root = binary_section_bytes(b"", &[TestInst::Nop], vec![]); - let file: BytecodeFile = - BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); - let mut machine = Machine::default(); - - machine.load_bytecode_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TestInst::Nop]); - assert!(machine.child.program.module.code.is_empty()); - assert!(machine.child.program.context.is_none()); - assert!(machine.default_child.program.module.code.is_empty()); - assert!(machine.default_child.program.context.is_none()); -} - -#[test] -fn text_generated_loadable_allows_missing_marked_children() { - let file = text_file( - &["child", "default_child"], - ".section(root):\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\ttest::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -.section(root).\n", - ); - let mut machine = TextMachine::default(); - - machine.load_sst_section(file.root()).unwrap(); - - assert_eq!(machine.program.module.code, vec![TextInst::Nop]); - assert!(machine.child.program.module.code.is_empty()); - assert!(machine.child.program.context.is_none()); - assert!(machine.default_child.program.module.code.is_empty()); - assert!(machine.default_child.program.context.is_none()); -} - -#[test] -fn binary_generated_loadable_rejects_unexpected_direct_children() { - let extra = binary_section_bytes(b"", &[], vec![]); - let root = binary_section_bytes(b"", &[TestInst::Nop], vec![(EXTRA_NAME, extra)]); - let file: BytecodeFile = - BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); - let mut machine = Machine::default(); - - let err = machine.load_bytecode_section(file.root()).unwrap_err(); - - assert!(err.to_string().contains("unexpected child section")); -} - -#[test] -fn text_generated_loadable_rejects_unexpected_direct_children() { - let file = text_file( - &["child", "default_child", "extra"], - ".section(root):\n\ -\t.text(root):\n\ -\t\tfn @main() {\n\ -\t\t\ttest::nop\n\ -\t\t}\n\ -\t.text(root).\n\ -\t.section(child):\n\ -\t.section(child).\n\ -\t.section(default_child):\n\ -\t.section(default_child).\n\ -\t.section(extra):\n\ -\t.section(extra).\n\ -.section(root).\n", - ); - let mut machine = TextMachine::default(); - - let err = machine.load_sst_section(file.root()).unwrap_err(); - - assert!(err.to_string().contains("unexpected child section")); -} - -fn binary_file_bytes(context: Vec, root: Vec) -> Vec { - let mut bytes = Vec::new(); - bytes.extend_from_slice(MAGIC); - bytes.write_u16::(VERSION).unwrap(); - bytes.write_u16::(FLAGS).unwrap(); - bytes - .write_u64::(context.len() as u64) - .unwrap(); - bytes.extend_from_slice(&context); - bytes.extend_from_slice(&root); - bytes -} - -fn text_file(section_names: &[&str], sections: &str) -> SstFile { - let context = section_names.join("\n"); - let context = if context.is_empty() { - String::new() - } else { - format!("{context}\n") - }; - SstFile::::from_text(&format!( - "sst v{VERSION}\n\n.global:\n{context}.global.\n\n{sections}" - )) - .unwrap() -} - -fn context_bytes() -> Vec { - b"child\ndefault_child\nextra\nmiddle\nleaf\n".to_vec() -} - -fn binary_section_bytes( - header: &[u8], - instructions: &[TestInst], - children: Vec<(u32, Vec)>, -) -> Vec { - let mut bytecode = Vec::new(); - for inst in instructions { - inst.write_bytes(&mut bytecode).unwrap(); - } - - let child_table_len = - CHILD_SECTION_TABLE_HEADER_LEN + children.len() * CHILD_SECTION_TABLE_ENTRY_LEN; - let bytecode_start = SECTION_FRAME_LEN + header.len() + SECTION_BYTECODE_HEADER_LEN; - let mut child_offset = bytecode_start + bytecode.len() + child_table_len; - let section_len = child_offset + children.iter().map(|(_, child)| child.len()).sum::(); - - let mut bytes = Vec::new(); - bytes.write_u64::(section_len as u64).unwrap(); - bytes - .write_u64::(header.len() as u64) - .unwrap(); - bytes.extend_from_slice(header); - bytes - .write_u64::(bytecode.len() as u64) - .unwrap(); - bytes.extend_from_slice(&bytecode); - bytes - .write_u32::(children.len() as u32) - .unwrap(); - for (name_index, child) in &children { - bytes.write_u32::(*name_index).unwrap(); - bytes - .write_u64::(child_offset as u64) - .unwrap(); - child_offset += child.len(); - } - for (_, child) in children { - bytes.extend_from_slice(&child); - } - bytes -} diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index 54974744..cfc5042a 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -3,7 +3,10 @@ use chumsky::Parser as _; use eyre::Result; -use vihaco::{Effects, Execute, Execution, Observe, StepResult, composite}; +use vihaco::{ + Effects, Execute, Execution, Observe, SstFile, SstGlobalContext, SstHeader, StepResult, + VERSION, composite, +}; use vihaco_parser::{Ident, Parse}; mod test_root { @@ -18,6 +21,30 @@ struct TestEffect; struct TestComponent; struct TestContext; +impl SstGlobalContext for TestContext { + fn from_text(_text: &str) -> Result { + Ok(Self) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, vihaco_parser_derive::Parse)] +#[syntax_class(type)] +enum TestType { + #[pattern = "`unit`"] + Unit, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +struct TestHeader; + +impl vihaco::FromText for TestHeader { + fn from_text(_text: &str) -> Result { + Ok(Self) + } +} + +impl SstHeader for TestHeader {} + impl Execute for TestComponent { type Message = TestMessage; type Effect = TestEffect; @@ -59,7 +86,7 @@ composite! { component: TestComponent, observer: TestObserver, #[program] - program: vihaco::ProgramImage, + program: vihaco::ProgramImage, } syntax { @@ -129,7 +156,7 @@ fn generated_program_loader_builds_and_installs_module() { header: (), functions: vec![vihaco::syntax::ParsedFunction { name: Ident("main".to_owned()), - params: Vec::>::new(), + params: Vec::>::new(), return_ty: None, body: vec![test_machine::syntax::Instruction::Run], }], @@ -141,7 +168,29 @@ fn generated_program_loader_builds_and_installs_module() { }; machine - .load_parsed(parsed, vihaco::ContextHandle::new(TestContext)) + .load_parsed::(parsed, vihaco::ContextHandle::new(TestContext)) + .unwrap(); + + assert_eq!(machine.program.module.code.len(), 1); + assert_eq!(machine.program.module.functions.len(), 1); + assert_eq!(machine.program.module.main_function, Some(0)); + assert_eq!(machine.program.pc, 0); +} + +#[test] +fn generated_source_loader_parses_and_installs_module() { + let file = SstFile::::from_text(&format!( + "sst v{VERSION}\n\n.section(root):\n\t.text(root):\n\t\tfn @main() {{\n\t\t\ttest::run\n\t\t}}\n\t.text(root).\n.section(root).\n" + )) + .unwrap(); + let mut machine = TestMachine { + component: TestComponent, + observer: TestObserver::default(), + program: vihaco::ProgramImage::new(), + }; + + machine + .load_source::(file.root()) .unwrap(); assert_eq!(machine.program.module.code.len(), 1); diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index d52ac7d1..6cca2a2a 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -116,9 +116,8 @@ message, observer, and handler failures. `#[device(code, alias = "name")]` contributes device metadata and source-symbol aliases. Codes must be unique. `#[loadable]` marks a device that receives a -direct child bytecode/SST section through the generated loader. A composite -that owns program data implements `LoadOwnBytecodeSection` or -`LoadOwnSstSection` in ordinary Rust. +direct child SST section through the generated loader. A composite that owns +program data implements `LoadOwnSstSection` in ordinary Rust. The composite macro can also declare structural composites with no `runtime` block. Those composites still provide fields, device @@ -179,6 +178,12 @@ let parsed = machine::syntax::ParsedModule::parse_section(section)?; machine.load_parsed(parsed, ContextHandle::new(MachineContext))?; ``` +For an SST section, provide the surface-type and header types explicitly: + +```rust ignore +machine.load_source::(section)?; +``` + `load_parsed` constructs a fresh module, lowers every function, records function metadata, selects `main`, installs the module and context, and resets the program counter. Malformed input or a lowering failure returns an error. From 79e02fa97204a1f33f2f9d0b812a957850143b6e Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 11 Aug 2026 10:27:23 -0400 Subject: [PATCH 10/15] Updated composite and component DSLs, making split on syntax and runtime instruction, value, and type resolution explicit (components now define syntax of their instructions, and resolution belongs to the composite --- Cargo.lock | 3 + crates/vihaco-module/src/loader.rs | 16 +- crates/vihaco-parser/src/lib.rs | 37 +- .../design/component-macro.md | 33 ++ .../design/composite_macro.md | 13 +- crates/vihaco-runtime-derive/src/composite.rs | 20 + .../src/composite/codegen.rs | 419 ++++++++++++++---- .../src/composite/loadable.rs | 46 +- .../src/composite/syntax.rs | 53 ++- .../src/composite/validate.rs | 67 ++- crates/vihaco-runtime/Cargo.toml | 2 + crates/vihaco-runtime/src/lib.rs | 4 + crates/vihaco-syntax/Cargo.toml | 1 + crates/vihaco-syntax/src/lib.rs | 51 ++- crates/vihaco-syntax/src/parse.rs | 49 +- crates/vihaco-syntax/src/resolve.rs | 10 +- crates/vihaco-syntax/src/types.rs | 43 +- crates/vihaco/src/lib.rs | 6 +- crates/vihaco/tests/component_syntax.rs | 57 +++ .../vihaco/tests/composite_syntax_codegen.rs | 110 +++++ crates/vihaco/tests/generated_sst_loading.rs | 133 +++--- .../tests/runtime_macro_crate_override.rs | 52 ++- design/crate-split.md | 2 +- vision/module-syntax-ownership.md | 50 +++ 24 files changed, 1016 insertions(+), 261 deletions(-) create mode 100644 crates/vihaco/tests/component_syntax.rs create mode 100644 crates/vihaco/tests/composite_syntax_codegen.rs create mode 100644 vision/module-syntax-ownership.md diff --git a/Cargo.lock b/Cargo.lock index ce536f39..6a28a23e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -744,7 +744,9 @@ dependencies = [ "vihaco-abi", "vihaco-bytecode", "vihaco-module", + "vihaco-parser", "vihaco-runtime-derive", + "vihaco-syntax", ] [[package]] @@ -772,6 +774,7 @@ version = "0.2.0" dependencies = [ "chumsky", "eyre", + "vihaco-abi", "vihaco-bytecode", "vihaco-parser", "vihaco-parser-derive", diff --git a/crates/vihaco-module/src/loader.rs b/crates/vihaco-module/src/loader.rs index 0d232542..8b16cc03 100644 --- a/crates/vihaco-module/src/loader.rs +++ b/crates/vihaco-module/src/loader.rs @@ -7,22 +7,22 @@ use vihaco_bytecode::{ConstantId, ContextHandle, SstSectionView}; use crate::host::{GetProgramInfo, ProgramCounter}; use crate::module::{FunctionInfo, LabelInfo, LocalModule, NoInfo, SourceSymbolInfo}; -/// Allow a machine to load the SST data owned directly by its section. +/// Allow a machine to load the SST program owned directly by its section. /// /// When used with the [`vihaco_runtime_derive::composite`] macro, this hook runs before /// generated child-section forwarding. Implement this for each composite to -/// make its own section behavior explicit, even when that behavior is a no-op. -pub trait LoadOwnSstSection { - fn load_own_sst_section<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; +/// make its own program behavior explicit, even when that behavior is a no-op. +pub trait LoadSstProgram { + fn load_sst_program<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; } -/// Allow a machine to load an SST section completely. +/// Allow a machine to load an SST subtree completely. /// /// For composites generated by [`vihaco_runtime_derive::composite`], this loads the -/// composite's own section through [`LoadOwnSstSection`] and then forwards +/// composite's own program through [`LoadSstProgram`] and then forwards /// direct child sections to `#[loadable]` devices. -pub trait LoadSstSection { - fn load_sst_section<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; +pub trait LoadSstSubtree { + fn load_sst_subtree<'bc>(&mut self, section: SstSectionView<'bc, C>) -> eyre::Result<()>; } /// Replace a program's loaded module, context, and program-counter state as one operation. diff --git a/crates/vihaco-parser/src/lib.rs b/crates/vihaco-parser/src/lib.rs index 46c1bb4d..021ef031 100644 --- a/crates/vihaco-parser/src/lib.rs +++ b/crates/vihaco-parser/src/lib.rs @@ -5,8 +5,8 @@ pub mod impls; pub use impls::{bare_token, ident, BareToken, Ident, QuotedString}; -use chumsky::error::Simple; -use chumsky::extra; +pub use chumsky::Parser; +pub use chumsky::{error::Simple, extra}; /// Marker for enums whose pattern-derived parser represents instruction /// syntax. @@ -15,6 +15,39 @@ use chumsky::extra; /// with `#[syntax_class(instruction, ...)]`. pub trait SurfaceInstruction {} +/// The optional source-syntax product owned by a runtime component. +/// +/// Components implement this contract when they provide local instruction, +/// value, and source-type syntax. The parser implementations are required for +/// every input lifetime so a composite can compose the products without +/// knowing how they are mounted. +pub trait InstructionSet { + /// The component's surface instruction syntax. + type Instruction: SurfaceInstruction + for<'src> Parse<'src>; + /// The component's operand/value syntax. + type Value: for<'src> Parse<'src>; + /// The component's source-type syntax. + type Type: for<'src> Parse<'src>; +} + +/// Prefix a component parser with a public composite namespace. +/// +/// This helper keeps generated syntax code independent of the parser crate's +/// implementation details while allowing a mounted component to retain its +/// local grammar. +pub fn namespaced_parser<'src, T>( + namespace: &'static str, +) -> impl Parser<'src, &'src str, T, extra::Err>> +where + T: Parse<'src> + 'src, +{ + chumsky::text::ascii::ident() + .to_slice() + .filter(move |name: &&str| *name == namespace) + .then_ignore(chumsky::primitive::just("::")) + .ignore_then(T::parser()) +} + /// A parser whose input is `&'src str` (char stream) and whose error type is `Simple`. /// /// The lifetime `'src` is the input lifetime. Output type `Self` is owned and does not borrow diff --git a/crates/vihaco-runtime-derive/design/component-macro.md b/crates/vihaco-runtime-derive/design/component-macro.md index 69bf8ce1..f574642d 100644 --- a/crates/vihaco-runtime-derive/design/component-macro.md +++ b/crates/vihaco-runtime-derive/design/component-macro.md @@ -70,6 +70,39 @@ The declaration contains runtime products only. Surface names and patterns are declared by a composite or a separate surface-instruction declaration selected by the composite. +The planned optional component syntax declaration is a sibling block after the +runtime `instruction` block. Its exact input shape is: + +```rust +syntax { + value LabelRef = "'@' ident"; + + value Value { + U32(u32), + Label(LabelRef), + } + + type Type { + I64 = "`i64`"; + U32 = "`u32`"; + } + + instruction { + Step(value: Value) = "'step $value"; + Branch(target: Value) = "'br $target"; + Add(ty: Type) = "'add $ty"; + Reset = "'reset"; + } +} +``` + +This block produces a component-local `syntax` module and an +`InstructionSet` implementation. It does not receive an alias, device code, +composite, or runtime route. Declarative parsing/code generation is reserved +for the composite syntax implementation; until then, components can provide +the same product with ordinary parser-derived types and a manual +`InstructionSet` implementation. + The syntax should eventually support named and tuple products as well: ```rust diff --git a/crates/vihaco-runtime-derive/design/composite_macro.md b/crates/vihaco-runtime-derive/design/composite_macro.md index 3e811134..b44362fb 100644 --- a/crates/vihaco-runtime-derive/design/composite_macro.md +++ b/crates/vihaco-runtime-derive/design/composite_macro.md @@ -327,10 +327,11 @@ borrow into the composite. ### `message with method` -The named method is implemented on the composite and receives the instruction payload: +The named method is implemented through the generated message-resolver trait and receives the +instruction payload: ```rust -impl Cpu { +impl cpu::runtime::MessageResolver for Cpu { fn resolve_add_message( &mut self, instruction: &Add, @@ -341,9 +342,9 @@ impl Cpu { } ``` -The macro calls the method uniformly even when the method does not need the instruction. This -keeps route-specific resolution explicit without introducing a route-parameterized -`ResolveMessage` trait. +The macro calls the trait method uniformly even when the method does not need the instruction. +This keeps route-specific resolution explicit while giving all `message with` routes one public +implementation boundary. ## Structural composites @@ -426,7 +427,7 @@ Cover at least: - a no-message route; - `message from` through `Supply`; -- `message with` through a composite method; +- `message with` through the generated message-resolver trait; - multiple routes sharing an instruction or effect type; - observers in declaration order; - `absorb with` delegation; diff --git a/crates/vihaco-runtime-derive/src/composite.rs b/crates/vihaco-runtime-derive/src/composite.rs index f78cdd4f..92d24462 100644 --- a/crates/vihaco-runtime-derive/src/composite.rs +++ b/crates/vihaco-runtime-derive/src/composite.rs @@ -130,4 +130,24 @@ mod tests { super::syntax::SyntaxMapping::Lower(_) )); } + + #[test] + fn parses_composite_owned_header_declaration() { + let declaration: CompositeDeclaration = parse_str( + r#" + composite Machine { + device: Device, + } + syntax { + header DeviceHeader => resolve_header; + } + "#, + ) + .unwrap(); + + let header = declaration.header.expect("header declaration"); + let header_ty = &header.ty; + assert_eq!(header.resolver.to_string(), "resolve_header"); + assert_eq!(quote::quote!(#header_ty).to_string(), "DeviceHeader"); + } } diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index 941282ec..ddf327a1 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -7,8 +7,8 @@ use quote::{format_ident, quote, quote_spanned}; use syn::{Field, Generics, Ident, Result, Type}; use super::syntax::{ - CompositeDeclaration, Handler, MessageSource, RouteDeclaration, SyntaxDeclaration, - SyntaxMapping, + CompositeDeclaration, Handler, HeaderDeclaration, MessageSource, RouteDeclaration, + SyntaxDeclaration, SyntaxMapping, }; use crate::common::{resolve_root, retain_generics}; @@ -29,11 +29,16 @@ fn marker_ident(variant: &Ident) -> Ident { format_ident!("__VihacoRoute_{name}") } +fn syntax_variant_ident(field: &Ident) -> Ident { + format_ident!("{}", field.to_string().to_case(Case::Pascal)) +} + fn strip_consumed_field_attrs(mut field: Field) -> Field { field.attrs.retain(|attr| { !attr.path().is_ident("device") && !attr.path().is_ident("loadable") && !attr.path().is_ident("program") + && !attr.path().is_ident("syntax") }); field } @@ -51,26 +56,55 @@ fn generate_syntax_module( root: &TokenStream2, generics: &Generics, error: Option<&Type>, + header: Option<&HeaderDeclaration>, syntax: &[SyntaxDeclaration], + fields: &[super::validate::FieldMetadata], ) -> TokenStream2 { - if syntax.is_empty() { + let mounts = fields + .iter() + .filter(|field| field.syntax.is_some()) + .collect::>(); + if syntax.is_empty() && mounts.is_empty() && header.is_none() { return quote! {}; } let enum_generics = syntax_generics(generics, syntax); - let variants = syntax.iter().map(|entry| { - let variant = &entry.variant; - let pattern = &entry.pattern; - let payload = entry - .payload - .as_ref() - .map(|payload| quote!((#payload))) - .unwrap_or_default(); - quote! { - #[pattern = #pattern] - #variant #payload - } - }); + let component_instruction_variants = mounts + .iter() + .map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + quote!(#variant(<#ty as #root::InstructionSet>::Instruction)) + }) + .collect::>(); + let component_value_variants = mounts + .iter() + .map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + quote!(#variant(<#ty as #root::InstructionSet>::Value)) + }) + .collect::>(); + let component_type_variants = mounts + .iter() + .map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + quote!(#variant(<#ty as #root::InstructionSet>::Type)) + }) + .collect::>(); + let public_variants = syntax + .iter() + .map(|entry| { + let variant = &entry.variant; + let payload = entry + .payload + .as_ref() + .map(|payload| quote!((#payload))) + .unwrap_or_default(); + quote!(#variant #payload) + }) + .collect::>(); let error = error .map(|error| quote!(#error)) .unwrap_or_else(|| quote!(::core::convert::Infallible)); @@ -89,19 +123,240 @@ fn generate_syntax_module( >; }) }); + let header_method = header.iter().map(|header| { + let ty = &header.ty; + let method = &header.resolver; + quote! { + fn #method( + &mut self, + header: #ty, + ) -> ::std::result::Result<(), #error>; + } + }); + + let helper_variants = syntax + .iter() + .map(|entry| { + let variant = &entry.variant; + let pattern = &entry.pattern; + let payload = entry + .payload + .as_ref() + .map(|payload| quote!((#payload))) + .unwrap_or_default(); + quote! { + #[pattern = #pattern] + #variant #payload + } + }) + .collect::>(); + let parser_alternatives = mounts + .iter() + .flat_map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + field + .syntax + .as_ref() + .expect("validated syntax mount") + .aliases + .iter() + .map(move |alias| { + let namespace = alias.value(); + quote! { + #root::namespaced_parser::< + <#ty as #root::InstructionSet>::Instruction + >(#namespace) + .map(Self::#variant) + } + }) + }) + .collect::>(); + let composite_parser = if syntax.is_empty() { + None + } else { + let arms = syntax.iter().map(|entry| { + let variant = &entry.variant; + if entry.payload.is_some() { + quote!(__VihacoCompositeInstruction::#variant(value) => Self::#variant(value)) + } else { + quote!(__VihacoCompositeInstruction::#variant => Self::#variant) + } + }); + Some(quote! { + __VihacoCompositeInstruction::parser().map(|instruction| match instruction { + #( #arms ),* + }) + }) + }; + let mut parser_alternatives = parser_alternatives; + if let Some(parser) = composite_parser { + parser_alternatives.push(parser); + } + let parser = parser_alternatives + .into_iter() + .reduce(|left, right| quote!(#left.or(#right))) + .expect("syntax module has a parser alternative"); + let component_value_parser = mounts.iter().flat_map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + field + .syntax + .as_ref() + .expect("validated syntax mount") + .aliases + .iter() + .map(move |alias| { + let namespace = alias.value(); + quote! { + #root::namespaced_parser::< + <#ty as #root::InstructionSet>::Value + >(#namespace) + .map(Self::#variant) + } + }) + }); + let component_type_parser = mounts.iter().flat_map(|field| { + let variant = syntax_variant_ident(&field.ident); + let ty = &field.ty; + field + .syntax + .as_ref() + .expect("validated syntax mount") + .aliases + .iter() + .map(move |alias| { + let namespace = alias.value(); + quote! { + #root::namespaced_parser::< + <#ty as #root::InstructionSet>::Type + >(#namespace) + .map(Self::#variant) + } + }) + }); + let value_parser = component_value_parser + .reduce(|left, right| quote!(#left.or(#right))) + .unwrap_or_else(|| quote!(#root::bare_token().map(|_| unreachable!()))); + let type_parser = component_type_parser + .reduce(|left, right| quote!(#left.or(#right))) + .unwrap_or_else(|| quote!(#root::bare_token().map(|_| unreachable!()))); + let helper_declaration = if syntax.is_empty() { + quote! {} + } else { + quote! { + #[derive(Clone, Debug, PartialEq, #root::Parse)] + #[syntax_class(instruction)] + enum __VihacoCompositeInstruction #enum_generics { + #( #helper_variants ),* + } + } + }; + + let header_declaration = header + .map(|header| { + let ty = &header.ty; + quote!(pub type Header = #ty;) + }) + .unwrap_or_else(|| { + quote! { + #[derive(Clone, Debug, PartialEq)] + pub struct Header; + + impl #root::FromText for Header { + fn from_text(_text: &str) -> ::eyre::Result { + Ok(Self) + } + } + + impl #root::SstHeader for Header {} + } + }); + let header_parser = header.iter().map(|_| { + quote! { + pub fn parse_header<'__vihaco_src, __VihacoContext>( + section: #root::SstSectionView<'__vihaco_src, __VihacoContext>, + ) -> ::eyre::Result
{ + section.parse_header::
() + } + } + }); quote! { pub mod syntax { use super::*; - #[derive(Clone, #root::Parse)] - #[syntax_class(instruction)] + use #root::Parser as _; + + #[derive(Clone, Debug, PartialEq)] + #[allow(non_camel_case_types)] pub enum Instruction #enum_generics { - #( #variants ),* + #( #component_instruction_variants, )* + #( #public_variants, )* + } + + impl #root::SurfaceInstruction for Instruction #enum_generics {} + + impl<'__vihaco_src> #root::Parse<'__vihaco_src> for Instruction #enum_generics { + fn parser() -> impl #root::Parser< + '__vihaco_src, + &'__vihaco_src str, + Self, + #root::extra::Err<#root::Simple<'__vihaco_src, char>>, + > { + #parser + } + } + + #[derive(Clone, Debug, PartialEq)] + pub enum Value { + #( #component_value_variants, )* + } + + impl<'__vihaco_src> #root::Parse<'__vihaco_src> for Value { + fn parser() -> impl #root::Parser< + '__vihaco_src, + &'__vihaco_src str, + Self, + #root::extra::Err<#root::Simple<'__vihaco_src, char>>, + > { + #value_parser + } + } + + #[derive(Clone, Debug, PartialEq)] + pub enum Type { + #( #component_type_variants, )* + } + + impl<'__vihaco_src> #root::Parse<'__vihaco_src> for Type { + fn parser() -> impl #root::Parser< + '__vihaco_src, + &'__vihaco_src str, + Self, + #root::extra::Err<#root::Simple<'__vihaco_src, char>>, + > { + #type_parser + } + } + + #header_declaration + #( #header_parser )* + + pub struct Module; + + impl #root::ModuleSyntax for Module { + type Instruction = Instruction #enum_generics; + type Value = Value; + type Type = Type; + type Header = Header; } pub trait Resolver { + #( #header_method )* #( #lowerer_methods )* } + + #helper_declaration } } } @@ -211,6 +466,7 @@ fn generate_program_loading( module: &Ident, generics: &Generics, error: Option<&Type>, + header: Option<&HeaderDeclaration>, syntax: &[SyntaxDeclaration], routes: &[RouteDeclaration], fields: &[super::validate::FieldMetadata], @@ -230,45 +486,54 @@ fn generate_program_loading( let instruction_ident = format_ident!("{name}Instruction"); let route_generics = retained_enum_generics(generics, routes); let (_, route_ty_generics, _) = route_generics.split_for_impl(); - let syntax_generics = syntax_generics(generics, syntax); - let (_, syntax_ty_generics, _) = syntax_generics.split_for_impl(); - let surface_ty = format_ident!("__VihacoSurfaceType"); - let header_ty = format_ident!("__VihacoHeader"); let context_ty = format_ident!("__VihacoContext"); let loadable_predicates = fields .iter() .filter(|field| field.loadable.is_some()) .map(|field| { let field_ty = &field.ty; - quote! { #field_ty: #root::loader::LoadSstSection<#context_ty> } + quote! { #field_ty: #root::loader::LoadSstSubtree<#context_ty> } }); + let header_resolution = header.iter().map(|header| { + let method = &header.resolver; + quote! { + ::#method(self, parsed.header) + .map_err(|error| ::eyre::eyre!( + "failed to resolve section header: {:?}", + error, + ))?; + } + }); quote! { - pub fn resolve_parsed<#surface_ty, #header_ty>( + pub fn resolve_parsed( &mut self, - parsed: #root::syntax::ParsedModule< - #module::syntax::Instruction #syntax_ty_generics, - #surface_ty, - #header_ty, - >, + parsed: #root::syntax::ParsedModule<#module::syntax::Module>, ) -> ::eyre::Result< <#program_ty as #root::BuildProgramModule>::Module, > where - #surface_ty: ::std::clone::Clone - + ::std::convert::Into< - <#program_ty as #root::BuildProgramModule>::Type, - >, + #module::syntax::Type: ::std::convert::Into< + <#program_ty as #root::BuildProgramModule>::Type, + >, #program_ty: #root::BuildProgramModule< Instruction = #instruction_ident #route_ty_generics, >, { + #( #header_resolution )* let mut module = <#program_ty as #root::BuildProgramModule>::empty_module(); for (function_index, function) in parsed.functions.into_iter().enumerate() { let start_address = <#program_ty as #root::BuildProgramModule>::instruction_count(&module); - for instruction in function.body { - let lowered = self.lower_surface_instruction(&instruction)?; + for (instruction_index, instruction) in function.body.into_iter().enumerate() { + let lowered = self + .lower_surface_instruction(&instruction) + .map_err(|error| ::eyre::eyre!( + "failed to lower function `{}` instruction {}: {:?}", + function.name.as_str(), + instruction_index, + error, + ))?; <#program_ty as #root::BuildProgramModule>::append_instructions( &mut module, lowered, @@ -291,7 +556,7 @@ fn generate_program_loading( ); #root::module::Parameter { name, - ty: param.ty.into(), + ty: param.ty.into(), } }) .collect(); @@ -322,20 +587,12 @@ fn generate_program_loading( .map_err(::std::convert::Into::<#error>::into) } - pub fn load_parsed<#surface_ty, #header_ty, #context_ty>( + pub fn load_parsed<#context_ty>( &mut self, - parsed: #root::syntax::ParsedModule< - #module::syntax::Instruction #syntax_ty_generics, - #surface_ty, - #header_ty, - >, + parsed: #root::syntax::ParsedModule<#module::syntax::Module>, context: #root::ContextHandle<#context_ty>, ) -> ::eyre::Result<()> where - #surface_ty: ::std::clone::Clone - + ::std::convert::Into< - <#program_ty as #root::BuildProgramModule>::Type, - >, #program_ty: #root::BuildProgramModule< Instruction = #instruction_ident #route_ty_generics, > + #root::InstallProgramModule< @@ -349,20 +606,15 @@ fn generate_program_loading( .map_err(::std::convert::Into::<#error>::into) } - pub fn load_source<'__vihaco_sst, #surface_ty, #header_ty, #context_ty>( + pub fn load_source<'__vihaco_sst, #context_ty>( &mut self, section: #root::SstSectionView<'__vihaco_sst, #context_ty>, ) -> ::eyre::Result<()> where - #module::syntax::Instruction #syntax_ty_generics: - #root::Parse<'__vihaco_sst> + '__vihaco_sst, - #surface_ty: #root::Parse<'__vihaco_sst> - + '__vihaco_sst - + ::std::clone::Clone - + ::std::convert::Into< - <#program_ty as #root::BuildProgramModule>::Type, - >, - #header_ty: #root::SstHeader, + #module::syntax::Instruction: #root::Parse<'__vihaco_sst> + '__vihaco_sst, + #module::syntax::Type: ::std::convert::Into< + <#program_ty as #root::BuildProgramModule>::Type, + >, #program_ty: #root::BuildProgramModule< Instruction = #instruction_ident #route_ty_generics, > + #root::InstallProgramModule< @@ -371,11 +623,8 @@ fn generate_program_loading( >, #( #loadable_predicates ),* { - let parsed = #root::syntax::ParsedModule::< - #module::syntax::Instruction #syntax_ty_generics, - #surface_ty, - #header_ty, - >::parse_section(section.clone())?; + let parsed = #root::syntax::ParsedModule::<#module::syntax::Module> + ::parse_section(section.clone())?; let module = self.resolve_parsed(parsed)?; <#program_ty as #root::InstallProgramModule<#context_ty>> ::install_program_module( @@ -393,6 +642,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result Result Result Result Result Result quote! { - self.#method(instruction) + ::#method(self, instruction) .map_err(::std::convert::Into::<#error_type>::into)? }, }; diff --git a/crates/vihaco-runtime-derive/src/composite/loadable.rs b/crates/vihaco-runtime-derive/src/composite/loadable.rs index d0b4feef..4b696789 100644 --- a/crates/vihaco-runtime-derive/src/composite/loadable.rs +++ b/crates/vihaco-runtime-derive/src/composite/loadable.rs @@ -21,13 +21,13 @@ pub(super) fn generate_loadable_impls( let (_, ty_generics, _) = generics.split_for_impl(); let own_sst_predicate = quote! { - #name #ty_generics: #root::loader::LoadOwnSstSection<#context> + #name #ty_generics: #root::loader::LoadSstProgram<#context> }; let sst_method_predicates: Vec<_> = loadables .iter() .map(|field| { let field_ty = &field.ty; - quote! { #field_ty: #root::loader::LoadSstSection<#context> } + quote! { #field_ty: #root::loader::LoadSstSubtree<#context> } }) .collect(); let sst_children: Vec<_> = loadables @@ -38,8 +38,8 @@ pub(super) fn generate_loadable_impls( let section_name = field.loadable.as_ref().expect("loadable field"); quote! { if let ::std::option::Option::Some(child) = section.child(#section_name) { - <#field_ty as #root::loader::LoadSstSection<#context>> - ::load_sst_section(&mut self.#field_ident, child)?; + <#field_ty as #root::loader::LoadSstSubtree<#context>> + ::load_sst_subtree(&mut self.#field_ident, child)?; } } }) @@ -48,7 +48,7 @@ pub(super) fn generate_loadable_impls( .iter() .map(|field| field.loadable.as_ref().expect("loadable field").as_str()) .collect(); - let expected_children = quote! { + let validate_children = quote! { let expected: &[&str] = &[#(#loadable_names),*]; for child in section.children() { let child_name = child.local_name().ok_or_else(|| { @@ -66,6 +66,9 @@ pub(super) fn generate_loadable_impls( } } }; + let forward_children = quote! { + #( #sst_children )* + }; let mut sst_impl_generics = generics.clone(); sst_impl_generics.params.push(syn::parse_quote!(#context)); @@ -78,7 +81,7 @@ pub(super) fn generate_loadable_impls( let field_ty = &field.ty; where_clause.predicates.push( syn::parse2(quote! { - #field_ty: #root::loader::LoadSstSection<#context> + #field_ty: #root::loader::LoadSstSubtree<#context> }) .expect("valid predicate"), ); @@ -89,21 +92,6 @@ pub(super) fn generate_loadable_impls( quote! { impl #impl_generics #name #ty_generics #where_clause { - pub fn load_generated_sst_sections<'__vihaco_sst, #context>( - &mut self, - section: #root::SstSectionView<'__vihaco_sst, #context>, - ) -> ::eyre::Result<()> - where - #name #ty_generics: #root::loader::LoadOwnSstSection<#context>, - #( #sst_method_predicates ),* - { - #root::loader::LoadOwnSstSection::<#context>::load_own_sst_section( - self, - section.clone(), - )?; - self.load_generated_sst_children(section) - } - pub fn load_generated_sst_children<'__vihaco_sst, #context>( &mut self, section: #root::SstSectionView<'__vihaco_sst, #context>, @@ -111,21 +99,27 @@ pub(super) fn generate_loadable_impls( where #( #sst_method_predicates ),* { - #expected_children - #( #sst_children )* + #validate_children + #forward_children Ok(()) } } - impl #sst_impl_generics #root::loader::LoadSstSection<#context> + impl #sst_impl_generics #root::loader::LoadSstSubtree<#context> for #name #ty_generics #sst_where_clause { - fn load_sst_section<'__vihaco_sst>( + fn load_sst_subtree<'__vihaco_sst>( &mut self, section: #root::SstSectionView<'__vihaco_sst, #context>, ) -> ::eyre::Result<()> { - self.load_generated_sst_sections(section) + #root::loader::LoadSstProgram::<#context>::load_sst_program( + self, + section.clone(), + )?; + #validate_children + #forward_children + Ok(()) } } } diff --git a/crates/vihaco-runtime-derive/src/composite/syntax.rs b/crates/vihaco-runtime-derive/src/composite/syntax.rs index 97c17a84..81f39ef8 100644 --- a/crates/vihaco-runtime-derive/src/composite/syntax.rs +++ b/crates/vihaco-runtime-derive/src/composite/syntax.rs @@ -21,6 +21,7 @@ syn::custom_keyword!(handle); syn::custom_keyword!(none); syn::custom_keyword!(from); syn::custom_keyword!(with); +syn::custom_keyword!(header); pub(super) struct CompositeDeclaration { pub(super) attrs: Vec, @@ -29,10 +30,16 @@ pub(super) struct CompositeDeclaration { pub(super) generics: Generics, pub(super) error: Option, pub(super) fields: Vec, + pub(super) header: Option, pub(super) syntax: Vec, pub(super) routes: Vec, } +pub(super) struct HeaderDeclaration { + pub(super) ty: Type, + pub(super) resolver: Ident, +} + pub(super) struct SyntaxDeclaration { pub(super) pattern: LitStr, pub(super) variant: Ident, @@ -70,6 +77,31 @@ pub(super) struct DeviceArgs { pub(super) aliases: Vec, } +pub(super) struct SyntaxArgs { + pub(super) aliases: Vec, +} + +impl Parse for SyntaxArgs { + fn parse(input: ParseStream<'_>) -> Result { + if input.is_empty() { + return Ok(Self { + aliases: Vec::new(), + }); + } + if input.peek(Token![=]) { + input.parse::()?; + return Ok(Self { + aliases: vec![input.parse()?], + }); + } + Ok(Self { + aliases: syn::punctuated::Punctuated::::parse_terminated(input)? + .into_iter() + .collect(), + }) + } +} + impl Parse for CompositeDeclaration { fn parse(input: ParseStream<'_>) -> Result { let attrs = Attribute::parse_outer(input)?; @@ -84,13 +116,13 @@ impl Parse for CompositeDeclaration { syn::braced!(body in input); let (error, fields) = parse_composite_body(&body)?; - let syntax = if input.peek(syntax) { + let (header, syntax) = if input.peek(syntax) { input.parse::()?; let syntax_body; syn::braced!(syntax_body in input); parse_syntax(&syntax_body)? } else { - Vec::new() + (None, Vec::new()) }; let routes = if input.peek(runtime) { @@ -120,13 +152,26 @@ impl Parse for CompositeDeclaration { generics, error, fields, + header, syntax, routes, }) } } -fn parse_syntax(input: ParseStream<'_>) -> Result> { +fn parse_syntax( + input: ParseStream<'_>, +) -> Result<(Option, Vec)> { + let header = if input.peek(header) { + input.parse::
()?; + let ty = input.parse::()?; + input.parse::]>()?; + let resolver = input.parse::()?; + input.parse::()?; + Some(HeaderDeclaration { ty, resolver }) + } else { + None + }; let mut declarations = Vec::new(); while !input.is_empty() { let attrs = Attribute::parse_outer(input)?; @@ -178,7 +223,7 @@ fn parse_syntax(input: ParseStream<'_>) -> Result> { mapping, }); } - Ok(declarations) + Ok((header, declarations)) } fn parse_composite_body(input: ParseStream<'_>) -> Result<(Option, Vec)> { diff --git a/crates/vihaco-runtime-derive/src/composite/validate.rs b/crates/vihaco-runtime-derive/src/composite/validate.rs index 0dbf0882..56d4b9a7 100644 --- a/crates/vihaco-runtime-derive/src/composite/validate.rs +++ b/crates/vihaco-runtime-derive/src/composite/validate.rs @@ -7,13 +7,15 @@ use syn::spanned::Spanned; use syn::{Field, Ident, LitStr, Result, Type}; use super::syntax::{ - DeviceArgs, Handler, MessageSource, RouteDeclaration, SyntaxDeclaration, SyntaxMapping, + DeviceArgs, Handler, MessageSource, RouteDeclaration, SyntaxArgs, SyntaxDeclaration, + SyntaxMapping, }; pub(super) struct FieldMetadata { pub(super) ident: Ident, pub(super) ty: Type, pub(super) device: Option, + pub(super) syntax: Option, pub(super) loadable: Option, pub(super) program: bool, } @@ -44,6 +46,7 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { let mut device = None; let mut loadable = None; let mut program = false; + let mut syntax = None; for attr in &field.attrs { if attr.path().is_ident("device") { if device.is_some() { @@ -75,6 +78,39 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { )); } program = true; + } else if attr.path().is_ident("syntax") { + if syntax.is_some() { + return Err(syn::Error::new( + attr.span(), + format!("duplicate syntax attribute on field `{ident}`"), + )); + } + let mut args = match &attr.meta { + syn::Meta::Path(_) => SyntaxArgs { + aliases: Vec::new(), + }, + syn::Meta::NameValue(value) => { + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(alias), + .. + }) = &value.value + else { + return Err(syn::Error::new( + value.value.span(), + "syntax namespace must be a string literal", + )); + }; + SyntaxArgs { + aliases: vec![alias.clone()], + } + } + syn::Meta::List(_) => attr.parse_args::()?, + }; + if args.aliases.is_empty() { + args.aliases + .push(syn::LitStr::new(&ident.to_string(), ident.span())); + } + syntax = Some(args); } } if loadable.is_some() && device.is_none() { @@ -87,6 +123,7 @@ pub(super) fn metadata_fields(fields: &[Field]) -> Result> { ident, ty: field.ty.clone(), device, + syntax, loadable, program, }); @@ -263,3 +300,31 @@ pub(super) fn validate_syntax( } Ok(()) } + +pub(super) fn validate_syntax_mounts(fields: &[FieldMetadata]) -> Result<()> { + let mut namespaces = BTreeMap::::new(); + for field in fields { + let Some(syntax) = &field.syntax else { + continue; + }; + for alias in &syntax.aliases { + let namespace = alias.value(); + if syn::parse_str::(&namespace).is_err() { + return Err(syn::Error::new( + alias.span(), + format!("syntax namespace `{namespace}` must be a Rust identifier"), + )); + } + if let Some(previous) = namespaces.insert(namespace.clone(), field.ident.clone()) { + return Err(syn::Error::new( + alias.span(), + format!( + "duplicate syntax namespace `{namespace}` for fields `{previous}` and `{}`", + field.ident + ), + )); + } + } + } + Ok(()) +} diff --git a/crates/vihaco-runtime/Cargo.toml b/crates/vihaco-runtime/Cargo.toml index ca83e2a7..b632233d 100644 --- a/crates/vihaco-runtime/Cargo.toml +++ b/crates/vihaco-runtime/Cargo.toml @@ -12,6 +12,8 @@ eyre = { workspace = true } vihaco-abi = { workspace = true } vihaco-bytecode = { workspace = true } vihaco-module = { workspace = true } +vihaco-parser = { workspace = true } +vihaco-syntax = { workspace = true } vihaco-runtime-derive = { workspace = true, optional = true } [features] diff --git a/crates/vihaco-runtime/src/lib.rs b/crates/vihaco-runtime/src/lib.rs index ec24cfe7..5b64f8a5 100644 --- a/crates/vihaco-runtime/src/lib.rs +++ b/crates/vihaco-runtime/src/lib.rs @@ -20,6 +20,10 @@ pub use vihaco_abi::traits::{EffectSink, Reset}; pub use vihaco_abi::{Effects, metadata}; pub use vihaco_bytecode::{BytecodeSectionView, SstSectionView}; pub use vihaco_module::loader; +pub use vihaco_parser::{ + InstructionSet, Parse, Parser, Simple, SurfaceInstruction, bare_token, extra, namespaced_parser, +}; +pub use vihaco_syntax::ModuleSyntax; pub use execute::{Execute, Execution, NoEffect, NoMessage, StepResult}; pub use generated::{CompositeMetadata, expect_exactly_one_effect}; diff --git a/crates/vihaco-syntax/Cargo.toml b/crates/vihaco-syntax/Cargo.toml index ec8dc37e..9dce2753 100644 --- a/crates/vihaco-syntax/Cargo.toml +++ b/crates/vihaco-syntax/Cargo.toml @@ -14,4 +14,5 @@ vihaco-bytecode = { workspace = true } vihaco-parser = { workspace = true } [dev-dependencies] +vihaco-abi = { workspace = true } vihaco-parser-derive = { workspace = true } diff --git a/crates/vihaco-syntax/src/lib.rs b/crates/vihaco-syntax/src/lib.rs index 95d13035..e4f14c9c 100644 --- a/crates/vihaco-syntax/src/lib.rs +++ b/crates/vihaco-syntax/src/lib.rs @@ -14,8 +14,8 @@ mod types; pub mod parse; pub mod resolve; -pub use types::{Param, ParsedFunction, ParsedModule}; -pub use vihaco_parser::SurfaceInstruction; +pub use types::{ModuleSyntax, Param, ParsedFunction, ParsedModule}; +pub use vihaco_parser::{InstructionSet, Parse, SurfaceInstruction}; pub use parse::{block_i64_flat, block_i64_pairs, skip}; pub use resolve::Resolve; @@ -24,6 +24,8 @@ pub use resolve::Resolve; mod tests { use super::*; use chumsky::Parser as _; + use vihaco_abi::traits::FromText; + use vihaco_bytecode::SstHeader; use vihaco_parser::Parse; // Minimal stub: an enum that derives Parse and has just two unit variants. @@ -44,10 +46,34 @@ mod tests { Unit, } + #[derive(Debug, Clone, PartialEq)] + struct StubHeader; + + impl FromText for StubHeader { + fn from_text(text: &str) -> eyre::Result { + if text.trim().is_empty() { + Ok(Self) + } else { + Err(eyre::eyre!("unexpected header text")) + } + } + } + + impl SstHeader for StubHeader {} + + struct StubSyntax; + + impl ModuleSyntax for StubSyntax { + type Instruction = StubInst; + type Value = (); + type Type = StubType; + type Header = StubHeader; + } + #[test] fn parses_empty_function() { let src = "fn @main() {}"; - let f = ParsedFunction::::parser() + let f = ParsedFunction::::parser() .parse(src) .into_result() .unwrap(); @@ -58,7 +84,7 @@ mod tests { #[test] fn parses_function_with_canonical_body() { let src = "fn @main() {\n stub::halt\n stub::print\n stub::halt\n}"; - let f = ParsedFunction::::parser() + let f = ParsedFunction::::parser() .parse(src) .into_result() .unwrap(); @@ -72,7 +98,7 @@ mod tests { fn rejects_unknown_instruction() { let src = "fn @main() { foo bar 1 2.0 }"; assert!( - ParsedFunction::::parser() + ParsedFunction::::parser() .parse(src) .has_errors() ); @@ -81,7 +107,7 @@ mod tests { #[test] fn parses_consumer_provided_return_type() { let src = "fn @main() -> unit { stub::halt }"; - let f = ParsedFunction::::parser() + let f = ParsedFunction::::parser() .parse(src) .into_result() .unwrap(); @@ -110,7 +136,7 @@ fn @main() { stub::halt } "; - let f = ParsedFunction::::parser() + let f = ParsedFunction::::parser() .parse(src) .into_result() .unwrap(); @@ -126,9 +152,18 @@ fn @main() { Dump(u32), } + struct OnlyOneSyntax; + + impl ModuleSyntax for OnlyOneSyntax { + type Instruction = OnlyOne; + type Value = (); + type Type = StubType; + type Header = StubHeader; + } + let src = "fn @main() { stub::dump foo }"; assert!( - ParsedFunction::::parser() + ParsedFunction::::parser() .parse(src) .has_errors() ); diff --git a/crates/vihaco-syntax/src/parse.rs b/crates/vihaco-syntax/src/parse.rs index 386e56b0..aea393a1 100644 --- a/crates/vihaco-syntax/src/parse.rs +++ b/crates/vihaco-syntax/src/parse.rs @@ -3,20 +3,17 @@ //! chumsky-0.10 combinators for the parsed-syntax shape. //! -//! `Parse` impls for `ParsedModule`/`ParsedFunction` are generic over the -//! consumer's instruction type `I`, source type `Ty`, and device-header type -//! `H`. +//! `Parse` impls for [`ParsedFunction`] are generic over a complete module +//! dialect. use chumsky::error::Simple; use chumsky::extra; use chumsky::prelude::*; use vihaco_parser::{Ident, Parse, QuotedString}; -use vihaco_bytecode::SstHeader; use vihaco_bytecode::SstSectionView; -use vihaco_parser::SurfaceInstruction; -use crate::{Param, ParsedFunction, ParsedModule}; +use crate::{ModuleSyntax, Param, ParsedFunction, ParsedModule}; type E<'src> = extra::Err>; @@ -101,34 +98,39 @@ pub fn block_i64_pairs<'src>() -> impl Parser<'src, &'src str, Vec<(i64, i64)>, } /// Parse `i64`/`f64`/etc. parameter list. Currently only accepts empty `()`. -fn param_list<'src, Ty>() -> impl Parser<'src, &'src str, Vec>, E<'src>> + Clone { +fn param_list<'src, S>() -> impl Parser<'src, &'src str, Vec>, E<'src>> + Clone +where + S: ModuleSyntax, +{ just('(') .padded() .then(just(')').padded()) .map(|_| Vec::new()) } -fn functions<'src, I, Ty>() -> impl Parser<'src, &'src str, Vec>, E<'src>> +fn functions<'src, S>() -> impl Parser<'src, &'src str, Vec>, E<'src>> where - I: SurfaceInstruction + Parse<'src> + 'src, - Ty: Parse<'src> + 'src, + S: ModuleSyntax, + S::Instruction: Parse<'src> + 'src, + S::Type: Parse<'src> + 'src, { skip() - .ignore_then(ParsedFunction::::parser()) + .ignore_then(ParsedFunction::::parser()) .repeated() .collect::>() .then_ignore(skip()) } -impl<'src, I, Ty> Parse<'src> for ParsedFunction +impl<'src, S> Parse<'src> for ParsedFunction where - I: SurfaceInstruction + Parse<'src> + 'src, - Ty: Parse<'src> + 'src, + S: ModuleSyntax, + S::Instruction: Parse<'src> + 'src, + S::Type: Parse<'src> + 'src, { fn parser() -> impl Parser<'src, &'src str, Self, E<'src>> { - let return_ty = just("->").padded().ignore_then(Ty::parser()).or_not(); + let return_ty = just("->").padded().ignore_then(S::Type::parser()).or_not(); let body = skip() - .ignore_then(I::parser()) + .ignore_then(S::Instruction::parser()) .repeated() .collect::>() .then_ignore(skip()); @@ -137,7 +139,7 @@ where .padded() .ignore_then(just('@')) .ignore_then(Ident::parser()) - .then(param_list::()) + .then(param_list::()) .then(return_ty) .then_ignore(just('{').padded()) .then(body) @@ -151,20 +153,19 @@ where } } -impl ParsedModule +impl ParsedModule where - I: SurfaceInstruction, + S: ModuleSyntax, { /// Parse a source section into a pre-resolution module. pub fn parse_section<'src, C>(section: SstSectionView<'src, C>) -> eyre::Result where - H: SstHeader, - I: vihaco_parser::Parse<'src> + 'src, - Ty: vihaco_parser::Parse<'src> + 'src, + S::Instruction: Parse<'src> + 'src, + S::Type: Parse<'src> + 'src, { - let header = section.parse_header::()?; + let header = section.parse_header::()?; let text = section.sst(); - let functions = functions::() + let functions = functions::() .parse(text) .into_result() .map_err(|errors| eyre::eyre!("failed to parse SST functions: {:?}", errors))?; diff --git a/crates/vihaco-syntax/src/resolve.rs b/crates/vihaco-syntax/src/resolve.rs index 85480e2c..ec802114 100644 --- a/crates/vihaco-syntax/src/resolve.rs +++ b/crates/vihaco-syntax/src/resolve.rs @@ -6,23 +6,21 @@ //! own instruction set, source type, and header type, holding whatever state //! is needed (label table, string interner, sugar expansion rules). -use vihaco_parser::SurfaceInstruction; - -use crate::ParsedModule; +use crate::{ModuleSyntax, ParsedModule}; /// Lower a parsed module to its resolved runtime form. /// /// Implementations own application-specific conversion such as translating /// typed surface variants, expanding explicitly modeled sugar, or interning /// parsed values. -pub trait Resolve +pub trait Resolve where - I: SurfaceInstruction, + S: ModuleSyntax, { /// Resolved module type — concrete to the consumer (typically /// `crate::module::Module` with consumer-specific /// `Info`). type Module; - fn resolve_module(&mut self, parsed: ParsedModule) -> eyre::Result; + fn resolve_module(&mut self, parsed: ParsedModule) -> eyre::Result; } diff --git a/crates/vihaco-syntax/src/types.rs b/crates/vihaco-syntax/src/types.rs index b05014d4..9585cd36 100644 --- a/crates/vihaco-syntax/src/types.rs +++ b/crates/vihaco-syntax/src/types.rs @@ -3,38 +3,53 @@ //! Parsed-syntax data shapes. See module docs in [`super`]. +use vihaco_bytecode::SstHeader; use vihaco_parser::{Ident, SurfaceInstruction}; +/// The complete source dialect for one SST module. +pub trait ModuleSyntax { + /// Surface instruction syntax accepted by this module. + type Instruction: SurfaceInstruction + std::fmt::Debug + Clone + PartialEq; + /// Source value syntax accepted by this module. + type Value; + /// Source type syntax accepted by this module. + type Type: std::fmt::Debug + Clone + PartialEq; + /// Parsed source syntax for this module's section header. + type Header: SstHeader + std::fmt::Debug + Clone + PartialEq; +} + /// Parsed `.sst` module before resolution. /// -/// `I` is the consumer's surface instruction type, `Ty` is its source type -/// syntax, and `H` is its section-header type. #[derive(Debug, Clone, PartialEq)] -pub struct ParsedModule +pub struct ParsedModule where - I: SurfaceInstruction, + S: ModuleSyntax, { - pub header: H, - pub functions: Vec>, + /// The parsed source header. This is distinct from installed runtime metadata. + pub header: S::Header, + pub functions: Vec>, } #[derive(Debug, Clone, PartialEq)] -pub struct ParsedFunction +pub struct ParsedFunction where - I: SurfaceInstruction, + S: ModuleSyntax, { /// Function name with the leading `@` stripped (`@main` → `"main"`). pub name: Ident, /// Empty for the moment — `.sst` examples don't exercise parameters. /// Non-empty parameter syntax errors during parsing. - pub params: Vec>, - /// Return type parsed with the consumer-provided `Ty` syntax. - pub return_ty: Option, - pub body: Vec, + pub params: Vec>, + /// Return type parsed with the module's source type syntax. + pub return_ty: Option, + pub body: Vec, } #[derive(Debug, Clone, PartialEq)] -pub struct Param { +pub struct Param +where + S: ModuleSyntax, +{ pub name: Ident, - pub ty: Ty, + pub ty: S::Type, } diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index 399a1c49..9a66f40d 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -36,7 +36,7 @@ pub use instruction_syntax::{ InstructionSugarVariantSyntax, OperandKind, SugarOperandKind, }; pub use loader::{ - BuildProgramModule, InstallProgramModule, LoadOwnSstSection, LoadSstSection, ProgramImage, + BuildProgramModule, InstallProgramModule, LoadSstProgram, LoadSstSubtree, ProgramImage, }; pub use macros::{Instruction, component, composite}; pub use program::{Type, Value}; @@ -46,8 +46,10 @@ pub use runtime::{ expect_exactly_one_effect, }; pub use traits::{FromBytes, FromText, GetProgramInfo, Reset}; -pub use vihaco_parser::{Parse, SurfaceInstruction}; +pub use vihaco_parser::{InstructionSet, Parse, SurfaceInstruction}; +pub use vihaco_parser::{Parser, Simple, bare_token, extra, namespaced_parser}; pub use vihaco_parser_derive::Parse; +pub use vihaco_syntax::ModuleSyntax; #[cfg(test)] mod public_api_tests { diff --git a/crates/vihaco/tests/component_syntax.rs b/crates/vihaco/tests/component_syntax.rs new file mode 100644 index 00000000..a8c62f1b --- /dev/null +++ b/crates/vihaco/tests/component_syntax.rs @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use chumsky::Parser as _; +use vihaco::{InstructionSet, Parse, SurfaceInstruction}; + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(value)] +enum LocalValue { + Number(i64), +} + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(type)] +enum LocalType { + #[pattern = "`i64`"] + I64, +} + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(instruction)] +enum LocalInstruction { + #[pattern = "'local::add $0"] + Add(i64), + #[pattern = "'local::reset"] + Reset, +} + +#[allow(dead_code)] +struct LocalInstructionSet; + +impl InstructionSet for LocalInstructionSet { + type Instruction = LocalInstruction; + type Value = LocalValue; + type Type = LocalType; +} + +fn require_surface_instruction() {} + +#[test] +fn local_syntax_is_parser_complete_without_mounting_context() { + require_surface_instruction::(); + assert_eq!( + LocalInstruction::parser() + .parse("local::add 7") + .into_result(), + Ok(LocalInstruction::Add(7)) + ); + assert_eq!( + LocalValue::parser().parse("42").into_result(), + Ok(LocalValue::Number(42)) + ); + assert_eq!( + LocalType::parser().parse("i64").into_result(), + Ok(LocalType::I64) + ); +} diff --git a/crates/vihaco/tests/composite_syntax_codegen.rs b/crates/vihaco/tests/composite_syntax_codegen.rs new file mode 100644 index 00000000..52ec77de --- /dev/null +++ b/crates/vihaco/tests/composite_syntax_codegen.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use chumsky::Parser as _; +use vihaco::{FromText, InstructionSet, ModuleSyntax, Parse, SstHeader, composite}; + +#[derive(Clone, Debug, PartialEq, vihaco::Parse)] +#[syntax_class(instruction)] +enum LocalInstruction { + #[pattern = "'step $0"] + Step(u32), +} + +#[derive(Clone, Debug, PartialEq, vihaco::Parse)] +#[syntax_class(type)] +enum LocalType { + #[pattern = "`unit`"] + Unit, +} + +#[derive(Clone, Debug, PartialEq, vihaco::Parse)] +#[syntax_class(value)] +enum LocalValue { + #[pattern = "`zero`"] + Zero, +} + +struct SyntaxComponent; + +impl InstructionSet for SyntaxComponent { + type Instruction = LocalInstruction; + type Value = LocalValue; + type Type = LocalType; +} + +struct RuntimeOnly; + +#[derive(Clone, Debug, PartialEq)] +pub struct MachineHeader; + +impl FromText for MachineHeader { + fn from_text(_text: &str) -> eyre::Result { + Ok(Self) + } +} + +impl SstHeader for MachineHeader {} + +composite! { + #[allow(dead_code)] + pub composite MountedSyntax { + #[device(0x01)] + #[syntax("left", "alias")] + left: SyntaxComponent, + + #[device(0x02)] + #[syntax("right")] + right: SyntaxComponent, + + #[device(0x03)] + runtime_only: RuntimeOnly, + } + syntax { + header MachineHeader => resolve_header; + } +} + +fn require_module_syntax() {} +fn require_machine_header>() {} + +#[test] +fn generated_syntax_wraps_mounts_and_aliases() { + require_module_syntax::(); + require_machine_header::(); + + let left = mounted_syntax::syntax::Instruction::parser() + .parse("left::step 7") + .into_result() + .unwrap(); + let alias = mounted_syntax::syntax::Instruction::parser() + .parse("alias::step 8") + .into_result() + .unwrap(); + let right = mounted_syntax::syntax::Instruction::parser() + .parse("right::step 9") + .into_result() + .unwrap(); + + assert!(matches!( + left, + mounted_syntax::syntax::Instruction::Left(LocalInstruction::Step(7)) + )); + assert!(matches!( + alias, + mounted_syntax::syntax::Instruction::Left(LocalInstruction::Step(8)) + )); + assert!(matches!( + right, + mounted_syntax::syntax::Instruction::Right(LocalInstruction::Step(9)) + )); +} + +#[test] +fn runtime_only_mount_does_not_require_instruction_set() { + let _: MountedSyntax = MountedSyntax { + left: SyntaxComponent, + right: SyntaxComponent, + runtime_only: RuntimeOnly, + }; +} diff --git a/crates/vihaco/tests/generated_sst_loading.rs b/crates/vihaco/tests/generated_sst_loading.rs index 3b844f7f..915c66a8 100644 --- a/crates/vihaco/tests/generated_sst_loading.rs +++ b/crates/vihaco/tests/generated_sst_loading.rs @@ -3,27 +3,11 @@ use eyre::Result; use vihaco::{ - ContextHandle, Effects, Execute, Execution, LoadOwnSstSection, LoadSstSection, NoEffect, - NoMessage, ProgramImage, SstFile, SstGlobalContext, SstHeader, SstSectionView, StepResult, - Type, Value, + ContextHandle, Effects, Execute, Execution, LoadSstProgram, LoadSstSubtree, NoEffect, + NoMessage, ProgramImage, SstFile, SstGlobalContext, SstSectionView, StepResult, Type, Value, syntax::{Param, ParsedFunction, ParsedModule}, - traits::FromText, }; use vihaco_parser::Ident; -use vihaco_parser_derive::Parse; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Parse)] -#[syntax_class(type)] -enum ParsedType { - #[pattern = "`i64`"] - I64, -} - -impl From for Type { - fn from(_value: ParsedType) -> Self { - Self::I64 - } -} #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RuntimeInstruction; @@ -48,6 +32,12 @@ impl Execute for TestComponent { } } +impl From for Type { + fn from(value: test_machine::syntax::Type) -> Self { + match value {} + } +} + #[derive(Debug, Clone, PartialEq, Eq)] struct TestContext { name: String, @@ -61,25 +51,14 @@ impl SstGlobalContext for TestContext { } } -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -struct NoHeader; - -impl SstHeader for NoHeader {} - -impl FromText for NoHeader { - fn from_text(_text: &str) -> Result { - Ok(Self) - } -} - #[derive(Debug, Default)] struct ChildLoader { loaded_sst: Option, context: Option>, } -impl LoadSstSection for ChildLoader { - fn load_sst_section<'src>(&mut self, section: SstSectionView<'src, TestContext>) -> Result<()> { +impl LoadSstSubtree for ChildLoader { + fn load_sst_subtree<'src>(&mut self, section: SstSectionView<'src, TestContext>) -> Result<()> { self.loaded_sst = Some(section.sst().to_owned()); self.context = Some(section.context_handle()); Ok(()) @@ -106,31 +85,34 @@ vihaco::composite! { syntax { #[pattern = "'test::run"] Run => runtime Run; + #[pattern = "'test::burst $0"] + Burst(u32) => lower_burst; } runtime { Run(RuntimeInstruction) => component { message none; + }, + Burst(RuntimeInstruction) => component { + message none; } } } -impl LoadOwnSstSection for TestMachine { - fn load_own_sst_section<'src>( +impl test_machine::syntax::Resolver for TestMachine { + fn lower_burst( &mut self, - section: SstSectionView<'src, TestContext>, - ) -> Result<()> { - let context = section.context_handle(); - let parsed = ParsedModule { - header: NoHeader, - functions: vec![ParsedFunction { - name: Ident("main".to_owned()), - params: Vec::>::new(), - return_ty: None, - body: vec![test_machine::syntax::Instruction::Run], - }], - }; - self.load_parsed(parsed, context) + count: u32, + ) -> std::result::Result, eyre::Report> { + Ok((0..count) + .map(|_| TestMachineInstruction::Burst(RuntimeInstruction)) + .collect()) + } +} + +impl LoadSstProgram for TestMachine { + fn load_sst_program<'src>(&mut self, section: SstSectionView<'src, TestContext>) -> Result<()> { + self.load_source(section) } } @@ -160,7 +142,7 @@ fn generated_sst_root_loads_program_and_forwards_children() { let context = file.context_handle(); let mut machine = TestMachine::default(); - machine.load_sst_section(file.root()).unwrap(); + machine.load_sst_subtree(file.root()).unwrap(); assert_eq!(machine.program.module.code.len(), 1); assert_eq!(machine.program.module.functions.len(), 1); @@ -183,6 +165,52 @@ fn generated_sst_root_loads_program_and_forwards_children() { ); } +#[test] +fn generated_load_parsed_installs_the_complete_module_dialect() { + let file = root_file( + ".section(root):\n\ +\t.text(root):\n\ +\t.text(root).\n\ +.section(root).\n", + ); + let context = file.context_handle(); + let mut machine = TestMachine::default(); + let parsed = ParsedModule { + header: test_machine::syntax::Header, + functions: vec![ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::>::new(), + return_ty: None, + body: vec![test_machine::syntax::Instruction::Run], + }], + }; + + machine.load_parsed(parsed, context).unwrap(); + + assert_eq!(machine.program.module.code.len(), 1); + assert!(machine.program.context.is_some()); +} + +#[test] +fn generated_load_source_expands_one_surface_instruction_to_many() { + let file = root_file( + ".section(root):\n\ +\t.text(root):\n\ +\t\tfn @main() {\n\ +\t\t\ttest::burst 3\n\ +\t\t}\n\ +\t.text(root).\n\ +.section(root).\n", + ); + let mut machine = TestMachine::default(); + + machine.load_source(file.root()).unwrap(); + + assert_eq!(machine.program.module.code.len(), 3); + assert_eq!(machine.program.module.functions[0].start_address, 0); + assert_eq!(machine.program.module.functions[0].end_address, 3); +} + #[test] fn malformed_root_source_is_rejected_before_program_installation() { let file = root_file( @@ -194,17 +222,10 @@ fn malformed_root_source_is_rejected_before_program_installation() { \t.text(root).\n\ .section(root).\n", ); - let result = - ParsedModule::::parse_section( - file.root(), - ); - let error = match result { - Ok(_) => panic!("malformed source unexpectedly parsed"), - Err(error) => error, - }; + let mut machine = TestMachine::default(); + let error = machine.load_source(file.root()).unwrap_err(); assert!(!error.to_string().is_empty()); - let machine = TestMachine::default(); assert!(machine.program.module.code.is_empty()); assert!(machine.program.context.is_none()); assert_eq!(machine.child.loaded_sst, None); diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index cfc5042a..b97f34cc 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -4,8 +4,7 @@ use chumsky::Parser as _; use eyre::Result; use vihaco::{ - Effects, Execute, Execution, Observe, SstFile, SstGlobalContext, SstHeader, StepResult, - VERSION, composite, + Effects, Execute, Execution, Observe, SstFile, SstGlobalContext, StepResult, VERSION, composite, }; use vihaco_parser::{Ident, Parse}; @@ -16,9 +15,11 @@ mod test_root { #[derive(Debug, Clone, Copy)] pub struct TestInstruction; -struct TestMessage; +pub struct TestMessage(u32); struct TestEffect; -struct TestComponent; +struct TestComponent { + received_message: Option, +} struct TestContext; impl SstGlobalContext for TestContext { @@ -34,17 +35,12 @@ enum TestType { Unit, } -#[derive(Debug, Clone, Copy, PartialEq)] -struct TestHeader; - -impl vihaco::FromText for TestHeader { - fn from_text(_text: &str) -> Result { - Ok(Self) +impl From for TestType { + fn from(value: test_machine::syntax::Type) -> Self { + match value {} } } -impl SstHeader for TestHeader {} - impl Execute for TestComponent { type Message = TestMessage; type Effect = TestEffect; @@ -53,8 +49,9 @@ impl Execute for TestComponent { fn execute( &mut self, _instruction: &TestInstruction, - _message: Self::Message, + message: Self::Message, ) -> Result, Self::Fault> { + self.received_message = Some(message.0); Ok(StepResult { effects: Effects::one(TestEffect), execution: Execution::Complete, @@ -107,11 +104,13 @@ composite! { } } -impl TestMachine { +impl test_machine::runtime::MessageResolver for TestMachine { fn resolve_message(&mut self, _instruction: &TestInstruction) -> Result { - Ok(TestMessage) + Ok(TestMessage(42)) } +} +impl TestMachine { fn handle_effect(&mut self, _effect: TestEffect) -> Result<()> { Ok(()) } @@ -135,7 +134,9 @@ fn runtime_macros_honor_explicit_crate_override() { assert!(matches!(parsed, test_machine::syntax::Instruction::Run)); let mut machine = TestMachine { - component: TestComponent, + component: TestComponent { + received_message: None, + }, observer: TestObserver::default(), program: vihaco::ProgramImage::new(), }; @@ -143,6 +144,7 @@ fn runtime_macros_honor_explicit_crate_override() { .execute_generated(&TestMachineInstruction::Run(TestInstruction)) .unwrap(); assert_eq!(outcome, Execution::Complete); + assert_eq!(machine.component.received_message, Some(42)); assert!(machine.observer.observed); let metadata = test_root::__private::GeneratedMachine::metadata(&machine); @@ -153,22 +155,24 @@ fn runtime_macros_honor_explicit_crate_override() { #[test] fn generated_program_loader_builds_and_installs_module() { let parsed = vihaco::syntax::ParsedModule { - header: (), + header: test_machine::syntax::Header, functions: vec![vihaco::syntax::ParsedFunction { name: Ident("main".to_owned()), - params: Vec::>::new(), + params: Vec::>::new(), return_ty: None, body: vec![test_machine::syntax::Instruction::Run], }], }; let mut machine = TestMachine { - component: TestComponent, + component: TestComponent { + received_message: None, + }, observer: TestObserver::default(), program: vihaco::ProgramImage::new(), }; machine - .load_parsed::(parsed, vihaco::ContextHandle::new(TestContext)) + .load_parsed(parsed, vihaco::ContextHandle::new(TestContext)) .unwrap(); assert_eq!(machine.program.module.code.len(), 1); @@ -184,14 +188,14 @@ fn generated_source_loader_parses_and_installs_module() { )) .unwrap(); let mut machine = TestMachine { - component: TestComponent, + component: TestComponent { + received_message: None, + }, observer: TestObserver::default(), program: vihaco::ProgramImage::new(), }; - machine - .load_source::(file.root()) - .unwrap(); + machine.load_source(file.root()).unwrap(); assert_eq!(machine.program.module.code.len(), 1); assert_eq!(machine.program.module.functions.len(), 1); diff --git a/design/crate-split.md b/design/crate-split.md index c228668c..f4a6eb99 100644 --- a/design/crate-split.md +++ b/design/crate-split.md @@ -82,7 +82,7 @@ Complete set of `::vihaco::…` paths emitted by the current derive: `CompositeMetadata`, `BytecodeSectionView`, `SstSectionView`. - `instruction::{OpCode, FromBytes, FromBytesWithOpcode, WriteBytes}` - `metadata::{DeviceMetadata, SourceSymbolAliasMetadata}` -- `loader::{LoadOwnBytecodeSection, LoadBytecodeSection, LoadOwnSstSection, LoadSstSection}` +- `loader::{LoadOwnBytecodeSection, LoadBytecodeSection, LoadSstProgram, LoadSstSubtree}` - `runtime::Message` - `__private::GeneratedMachine` diff --git a/vision/module-syntax-ownership.md b/vision/module-syntax-ownership.md new file mode 100644 index 00000000..92cecd41 --- /dev/null +++ b/vision/module-syntax-ownership.md @@ -0,0 +1,50 @@ +# Module syntax rewrite ownership map + +This map records the initial disjoint ownership boundaries for the parallel +implementation tracks in `module-syntax-plan.md`. Agents must inspect the +current state before editing and report any required boundary change before +touching another track's implementation files. + +## Agent A — `vihaco-syntax` contract + +- `crates/vihaco-syntax/src/lib.rs` +- `crates/vihaco-syntax/src/types.rs` +- `crates/vihaco-syntax/src/parse.rs` +- `crates/vihaco-syntax/src/resolve.rs` +- focused tests contained in the files above + +## Agent B — component instruction-set syntax API + +- component-side public API files under `crates/vihaco-runtime/src/` +- component derive API/codegen files under `crates/vihaco-runtime-derive/src/component*` +- component/parser-focused tests under `crates/vihaco-runtime/tests/` and + `crates/vihaco/tests/component_macro.rs` + +Agent B must not edit `crates/vihaco-runtime-derive/src/composite/`. + +## Agent C — loader capability rename + +- `crates/vihaco-module/src/loader.rs` +- `crates/vihaco-module/src/lib.rs` +- loader-focused tests owned by `crates/vihaco-module` + +Agent C may update direct in-repository consumers only where necessary to +complete the rename, coordinating conflicts with the coordinator rather than +changing generated composite behavior. + +## Later sequenced ownership + +- Agent D: `crates/vihaco-runtime-derive/src/composite/` syntax/codegen and + dedicated macro fixtures; starts after A and B. +- Agent E: composite header syntax/resolution and metadata builder seams; + starts after the contract and codegen interfaces are available. +- Agent F: generated resolver/loading integration, primarily composite + `codegen.rs` and generated SST loading tests; starts after A, C, and D. +- Agent G: nested generated loading and nested fixtures; starts after C and F. +- Agent H: semantic lowering/resolution tests and implementation seams; starts + after F. +- Agent I: cross-crate regression coverage; starts after F, G, and H. +- Agent J: demo, guide, and companion-plan migration; starts after I. + +The coordinator owns conflict resolution, public API consistency, and all +integration-gate verification. From 51b2aa8404233dc3a42326e313e730258e757fe8 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 11 Aug 2026 11:35:22 -0400 Subject: [PATCH 11/15] Migrated demos to new loader API, updated docs --- .../design/component-macro.md | 4 +- crates/vihaco-runtime-derive/src/component.rs | 222 +++++++- .../src/composite/codegen.rs | 110 +++- .../src/composite/loadable.rs | 32 +- crates/vihaco-syntax/Cargo.toml | 1 + crates/vihaco-syntax/src/lib.rs | 4 +- crates/vihaco-syntax/src/parse.rs | 9 +- crates/vihaco-syntax/src/types.rs | 17 + crates/vihaco/tests/generated_sst_loading.rs | 280 +++++++++- .../vihaco/tests/module_syntax_regressions.rs | 345 ++++++++++++ crates/vihaco/tests/multi_route_composite.rs | 263 +++++++++ .../tests/runtime_macro_crate_override.rs | 4 + demos/examples/demo.md | 27 +- demos/examples/demo.rs | 74 +-- demos/examples/demo/src/cpu.rs | 142 ++++- demos/examples/demo/src/driver.rs | 3 +- demos/examples/demo/src/surface.rs | 57 -- demos/examples/demo/stdlib/arithmetic.rs | 17 + demos/examples/demo/stdlib/channel.rs | 22 + demos/examples/demo/stdlib/debug_trace.rs | 13 + docs/src/pages/guide/composites.md | 4 +- docs/src/pages/guide/messages.md | 4 +- docs/src/pages/guide/parser-advanced.md | 49 +- vision/composite-syntax-runtime-plan.md | 513 +++++++++++++----- vision/execution-pipeline.md | 16 +- vision/macro-generation.md | 2 +- vision/module-syntax-plan-original.md | 511 +++++++++++++++++ vision/module-syntax-plan.md | 414 ++++++++++++++ vision/sst-resolution.md | 10 +- vision/types-and-values.md | 24 +- 30 files changed, 2876 insertions(+), 317 deletions(-) create mode 100644 crates/vihaco/tests/module_syntax_regressions.rs create mode 100644 crates/vihaco/tests/multi_route_composite.rs delete mode 100644 demos/examples/demo/src/surface.rs create mode 100644 vision/module-syntax-plan-original.md create mode 100644 vision/module-syntax-plan.md diff --git a/crates/vihaco-runtime-derive/design/component-macro.md b/crates/vihaco-runtime-derive/design/component-macro.md index f574642d..e010d95f 100644 --- a/crates/vihaco-runtime-derive/design/component-macro.md +++ b/crates/vihaco-runtime-derive/design/component-macro.md @@ -70,8 +70,8 @@ The declaration contains runtime products only. Surface names and patterns are declared by a composite or a separate surface-instruction declaration selected by the composite. -The planned optional component syntax declaration is a sibling block after the -runtime `instruction` block. Its exact input shape is: +An optional component syntax declaration is a sibling block after the runtime +`instruction` block. Its input shape is: ```rust syntax { diff --git a/crates/vihaco-runtime-derive/src/component.rs b/crates/vihaco-runtime-derive/src/component.rs index efbd9d04..a65ed2f7 100644 --- a/crates/vihaco-runtime-derive/src/component.rs +++ b/crates/vihaco-runtime-derive/src/component.rs @@ -12,6 +12,8 @@ use syn::{Attribute, Field, Fields, Generics, Ident, Result, Token, Visibility, syn::custom_keyword!(component); syn::custom_keyword!(instruction); +syn::custom_keyword!(syntax); +syn::custom_keyword!(value); struct ComponentDeclaration { module: Option, @@ -20,6 +22,33 @@ struct ComponentDeclaration { generics: Generics, state: Fields, products: Vec, + syntax: Option, +} + +struct ComponentSyntax { + type_declaration: Option, + value_declaration: Option, + instruction_declaration: Option, +} + +struct SyntaxEnum { + name: Ident, + variants: Vec, +} + +struct SyntaxVariant { + name: Ident, + pattern: syn::LitStr, +} + +struct SyntaxInstruction { + variants: Vec, +} + +struct SyntaxInstructionVariant { + name: Ident, + fields: Vec, + pattern: syn::LitStr, } struct InstructionProduct { @@ -55,6 +84,15 @@ impl Parse for ComponentDeclaration { Vec::new() }; + let syntax_declaration = if input.peek(syntax) { + input.parse::()?; + let content; + syn::braced!(content in input); + Some(content.parse()?) + } else { + None + }; + if !input.is_empty() { return Err(input.error("unexpected tokens after component declaration")); } @@ -66,10 +104,107 @@ impl Parse for ComponentDeclaration { generics, state, products, + syntax: syntax_declaration, }) } } +impl Parse for ComponentSyntax { + fn parse(input: ParseStream<'_>) -> Result { + let mut type_declaration = None; + let mut value_declaration = None; + let mut instruction_declaration = None; + + while !input.is_empty() { + if input.peek(Token![type]) { + input.parse::()?; + let declaration = parse_syntax_enum(input)?; + if type_declaration.replace(declaration).is_some() { + return Err(input.error("duplicate component syntax type declaration")); + } + } else if input.peek(value) { + input.parse::()?; + let declaration = parse_syntax_enum(input)?; + if value_declaration.replace(declaration).is_some() { + return Err(input.error("duplicate component syntax value declaration")); + } + } else if input.peek(instruction) { + input.parse::()?; + let content; + syn::braced!(content in input); + let declaration = SyntaxInstruction { + variants: parse_syntax_instruction_variants(&content)?, + }; + if instruction_declaration.replace(declaration).is_some() { + return Err(input.error("duplicate component syntax instruction declaration")); + } + } else { + return Err(input.error("expected `type`, `value`, or `instruction`")); + } + } + + Ok(Self { + type_declaration, + value_declaration, + instruction_declaration, + }) + } +} + +fn parse_syntax_enum(input: ParseStream<'_>) -> Result { + let name = input.parse()?; + let content; + syn::braced!(content in input); + let mut variants = Vec::new(); + while !content.is_empty() { + let variant = SyntaxVariant { + name: content.parse()?, + pattern: { + content.parse::()?; + content.parse()? + }, + }; + variants.push(variant); + if content.peek(Token![,]) { + content.parse::()?; + } else if content.peek(Token![;]) { + content.parse::()?; + } + } + Ok(SyntaxEnum { name, variants }) +} + +fn parse_syntax_instruction_variants( + input: ParseStream<'_>, +) -> Result> { + let mut variants = Vec::new(); + while !input.is_empty() { + let name = input.parse()?; + let fields = if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + syn::punctuated::Punctuated::::parse_terminated(&content)? + .into_iter() + .collect() + } else { + Vec::new() + }; + input.parse::()?; + let pattern = input.parse()?; + variants.push(SyntaxInstructionVariant { + name, + fields, + pattern, + }); + if input.peek(Token![,]) { + input.parse::()?; + } else if input.peek(Token![;]) { + input.parse::()?; + } + } + Ok(variants) +} + impl Parse for InstructionProduct { fn parse(input: ParseStream<'_>) -> Result { let attrs = Attribute::parse_outer(input)?; @@ -163,6 +298,16 @@ fn validate(declaration: &ComponentDeclaration, module: &Ident) -> Result<()> { )); } } + if let Some(syntax) = &declaration.syntax + && (syntax.type_declaration.is_none() + || syntax.value_declaration.is_none() + || syntax.instruction_declaration.is_none()) + { + return Err(syn::Error::new( + module.span(), + "component syntax requires `type`, `value`, and `instruction` declarations", + )); + } Ok(()) } @@ -240,11 +385,12 @@ pub fn expand(input: TokenStream) -> TokenStream { generics, state, products, + syntax, .. } = declaration; let state = parent_visible_fields(state); let visibility = public_by_default(visibility); - let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let products = products.into_iter().map(|product| { let InstructionProduct { attrs, @@ -272,6 +418,78 @@ pub fn expand(input: TokenStream) -> TokenStream { #declaration } }); + let syntax = syntax.map(|syntax| { + let type_declaration = syntax.type_declaration.expect("validated component syntax"); + let value_declaration = syntax + .value_declaration + .expect("validated component syntax"); + let instruction_declaration = syntax + .instruction_declaration + .expect("validated component syntax"); + let type_name = type_declaration.name; + let value_name = value_declaration.name; + let type_variants = type_declaration.variants.into_iter().map(|variant| { + let name = variant.name; + let pattern = variant.pattern; + quote! { + #[pattern = #pattern] + #name + } + }); + let value_variants = value_declaration.variants.into_iter().map(|variant| { + let name = variant.name; + let pattern = variant.pattern; + quote! { + #[pattern = #pattern] + #name + } + }); + let instruction_variants = instruction_declaration.variants.into_iter().map(|variant| { + let name = variant.name; + let pattern = variant.pattern; + let fields = variant.fields; + if fields.is_empty() { + quote! { + #[pattern = #pattern] + #name + } + } else { + quote! { + #[pattern = #pattern] + #name(#(#fields),*) + } + } + }); + quote! { + #visibility mod syntax { + use super::*; + + #[derive(Clone, Debug, PartialEq, ::vihaco::Parse)] + #[syntax_class(type)] + #visibility enum #type_name { + #( #type_variants, )* + } + + #[derive(Clone, Debug, PartialEq, ::vihaco::Parse)] + #[syntax_class(value)] + #visibility enum #value_name { + #( #value_variants, )* + } + + #[derive(Clone, Debug, PartialEq, ::vihaco::Parse)] + #[syntax_class(instruction)] + #visibility enum Instruction { + #( #instruction_variants, )* + } + } + + impl #impl_generics ::vihaco::InstructionSet for #name #ty_generics #where_clause { + type Instruction = syntax::Instruction; + type Value = syntax::#value_name; + type Type = syntax::#type_name; + } + } + }); quote! { #visibility mod #module_name { @@ -284,6 +502,8 @@ pub fn expand(input: TokenStream) -> TokenStream { #( #products )* } + + #syntax } } .into() diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index ddf327a1..aab10e43 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -58,6 +58,7 @@ fn generate_syntax_module( error: Option<&Type>, header: Option<&HeaderDeclaration>, syntax: &[SyntaxDeclaration], + routes: &[RouteDeclaration], fields: &[super::validate::FieldMetadata], ) -> TokenStream2 { let mounts = fields @@ -123,6 +124,21 @@ fn generate_syntax_module( >; }) }); + let component_lowerer_methods = routes.iter().next().into_iter().flat_map(|_| { + mounts.iter().map(|field| { + let method = format_ident!("lower_{}", field.ident); + let ty = &field.ty; + quote! { + fn #method( + &mut self, + instruction: <#ty as #root::InstructionSet>::Instruction, + ) -> ::std::result::Result< + ::std::vec::Vec, + #error, + >; + } + }) + }); let header_method = header.iter().map(|header| { let ty = &header.ty; let method = &header.resolver; @@ -354,6 +370,7 @@ fn generate_syntax_module( pub trait Resolver { #( #header_method )* #( #lowerer_methods )* + #( #component_lowerer_methods )* } #helper_declaration @@ -368,9 +385,9 @@ fn generate_resolver_traits( routes: &[RouteDeclaration], fields: &[super::validate::FieldMetadata], ) -> TokenStream2 { - let Some(error) = error else { + if error.is_none() { return quote! {}; - }; + } let field_ty = |field: &Ident| -> &Type { &fields .iter() @@ -407,8 +424,13 @@ fn generate_surface_lowering( syntax: &[SyntaxDeclaration], routes: &[RouteDeclaration], error: Option<&Type>, + fields: &[super::validate::FieldMetadata], ) -> TokenStream2 { - if syntax.is_empty() || routes.is_empty() || error.is_none() { + let mounts = fields + .iter() + .filter(|field| field.syntax.is_some()) + .collect::>(); + if (syntax.is_empty() && mounts.is_empty()) || routes.is_empty() || error.is_none() { return quote! {}; } let error = error.expect("checked above"); @@ -443,6 +465,16 @@ fn generate_surface_lowering( } }) }); + let component_lowerer_arms = mounts.iter().map(|field| { + let variant = syntax_variant_ident(&field.ident); + let method = format_ident!("lower_{}", field.ident); + quote! { + #module::syntax::Instruction::#variant(instruction) => { + ::#method(self, instruction) + .map_err(::std::convert::Into::<#error>::into) + } + } + }); quote! { fn lower_surface_instruction( &mut self, @@ -454,6 +486,7 @@ fn generate_surface_lowering( match instruction.clone() { #( #arms, )* #( #lowerer_arms, )* + #( #component_lowerer_arms, )* } } } @@ -471,13 +504,14 @@ fn generate_program_loading( routes: &[RouteDeclaration], fields: &[super::validate::FieldMetadata], ) -> TokenStream2 { - let Some(error) = error else { + if error.is_none() { return quote! {}; - }; + } let Some(program) = fields.iter().find(|field| field.program) else { return quote! {}; }; - if syntax.is_empty() || routes.is_empty() { + let has_component_syntax = fields.iter().any(|field| field.syntax.is_some()); + if (syntax.is_empty() && !has_component_syntax) || routes.is_empty() { return quote! {}; } @@ -520,8 +554,42 @@ fn generate_program_loading( Instruction = #instruction_ident #route_ty_generics, >, { + let #root::syntax::ParsedModule { + header, + functions, + labels, + constants, + strings, + source_symbols, + } = parsed; + let parsed = #root::syntax::ParsedModule { + header, + functions, + labels, + constants, + strings, + source_symbols, + }; #( #header_resolution )* let mut module = <#program_ty as #root::BuildProgramModule>::empty_module(); + for string in parsed.strings { + <#program_ty as #root::BuildProgramModule>::intern_string(&mut module, string); + } + for constant in parsed.constants { + <#program_ty as #root::BuildProgramModule>::add_constant( + &mut module, + constant, + ); + } + for source_symbol in parsed.source_symbols { + <#program_ty as #root::BuildProgramModule>::add_source_symbol( + &mut module, + #root::module::SourceSymbolInfo { + index: source_symbol.index, + name: source_symbol.name.as_str().to_owned(), + }, + ); + } for (function_index, function) in parsed.functions.into_iter().enumerate() { let start_address = <#program_ty as #root::BuildProgramModule>::instruction_count(&module); @@ -529,9 +597,10 @@ fn generate_program_loading( let lowered = self .lower_surface_instruction(&instruction) .map_err(|error| ::eyre::eyre!( - "failed to lower function `{}` instruction {}: {:?}", + "failed to lower function `{}` instruction {} ({:?}): {}", function.name.as_str(), instruction_index, + instruction, error, ))?; <#program_ty as #root::BuildProgramModule>::append_instructions( @@ -576,6 +645,19 @@ fn generate_program_loading( file: 0, }, ); + for label in parsed.labels.iter().filter(|label| label.function == function.name) { + let name = <#program_ty as #root::BuildProgramModule>::intern_string( + &mut module, + label.name.as_str().to_owned(), + ); + <#program_ty as #root::BuildProgramModule>::add_label( + &mut module, + #root::module::LabelInfo { + address: start_address + label.instruction, + name, + }, + ); + } if function.name.as_str() == "main" { <#program_ty as #root::BuildProgramModule>::set_main_function( &mut module, @@ -584,7 +666,6 @@ fn generate_program_loading( } } <#program_ty as #root::BuildProgramModule>::finish(module) - .map_err(::std::convert::Into::<#error>::into) } pub fn load_parsed<#context_ty>( @@ -603,7 +684,6 @@ fn generate_program_loading( let module = self.resolve_parsed(parsed)?; <#program_ty as #root::InstallProgramModule<#context_ty>> ::install_program_module(&mut self.#program_field, module, context) - .map_err(::std::convert::Into::<#error>::into) } pub fn load_source<'__vihaco_sst, #context_ty>( @@ -626,14 +706,14 @@ fn generate_program_loading( let parsed = #root::syntax::ParsedModule::<#module::syntax::Module> ::parse_section(section.clone())?; let module = self.resolve_parsed(parsed)?; + let children_section = section.clone(); + self.load_generated_sst_children(children_section)?; <#program_ty as #root::InstallProgramModule<#context_ty>> ::install_program_module( &mut self.#program_field, module, section.context_handle(), ) - .map_err(::std::convert::Into::<#error>::into)?; - self.load_generated_sst_children(section) } } } @@ -687,6 +767,7 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result Result Result Result> ::load_sst_subtree(&mut self.#field_ident, child)?; } @@ -50,6 +57,7 @@ pub(super) fn generate_loadable_impls( .collect(); let validate_children = quote! { let expected: &[&str] = &[#(#loadable_names),*]; + let mut seen: ::std::vec::Vec<::std::string::String> = ::std::vec::Vec::new(); for child in section.children() { let child_name = child.local_name().ok_or_else(|| { ::eyre::eyre!( @@ -57,6 +65,13 @@ pub(super) fn generate_loadable_impls( section.display_path(), ) })?; + if seen.iter().any(|seen| seen == child_name) { + return Err(::eyre::eyre!( + "section `{}` has duplicate child section `{}`", + section.display_path(), + child.display_path(), + )); + } if !expected.iter().any(|expected| *expected == child_name) { return Err(::eyre::eyre!( "section `{}` has unexpected child section `{}`", @@ -64,6 +79,16 @@ pub(super) fn generate_loadable_impls( child.display_path(), )); } + seen.push(child_name.to_owned()); + } + for expected_name in expected { + if !seen.iter().any(|seen| seen == expected_name) { + return Err(::eyre::eyre!( + "section `{}` is missing expected child section `{}`", + section.display_path(), + expected_name, + )); + } } }; let forward_children = quote! { @@ -113,12 +138,11 @@ pub(super) fn generate_loadable_impls( &mut self, section: #root::SstSectionView<'__vihaco_sst, #context>, ) -> ::eyre::Result<()> { + let program_section = section.clone(); #root::loader::LoadSstProgram::<#context>::load_sst_program( self, - section.clone(), + program_section, )?; - #validate_children - #forward_children Ok(()) } } diff --git a/crates/vihaco-syntax/Cargo.toml b/crates/vihaco-syntax/Cargo.toml index 9dce2753..dc15bc6f 100644 --- a/crates/vihaco-syntax/Cargo.toml +++ b/crates/vihaco-syntax/Cargo.toml @@ -11,6 +11,7 @@ authors.workspace = true chumsky = { workspace = true } eyre = { workspace = true } vihaco-bytecode = { workspace = true } +vihaco-abi = { workspace = true } vihaco-parser = { workspace = true } [dev-dependencies] diff --git a/crates/vihaco-syntax/src/lib.rs b/crates/vihaco-syntax/src/lib.rs index e4f14c9c..cd51e8b1 100644 --- a/crates/vihaco-syntax/src/lib.rs +++ b/crates/vihaco-syntax/src/lib.rs @@ -14,7 +14,9 @@ mod types; pub mod parse; pub mod resolve; -pub use types::{ModuleSyntax, Param, ParsedFunction, ParsedModule}; +pub use types::{ + ModuleSyntax, Param, ParsedFunction, ParsedLabel, ParsedModule, ParsedSourceSymbol, +}; pub use vihaco_parser::{InstructionSet, Parse, SurfaceInstruction}; pub use parse::{block_i64_flat, block_i64_pairs, skip}; diff --git a/crates/vihaco-syntax/src/parse.rs b/crates/vihaco-syntax/src/parse.rs index aea393a1..89e51c5c 100644 --- a/crates/vihaco-syntax/src/parse.rs +++ b/crates/vihaco-syntax/src/parse.rs @@ -170,6 +170,13 @@ where .into_result() .map_err(|errors| eyre::eyre!("failed to parse SST functions: {:?}", errors))?; - Ok(Self { header, functions }) + Ok(Self { + header, + functions, + labels: Vec::new(), + constants: Vec::new(), + strings: Vec::new(), + source_symbols: Vec::new(), + }) } } diff --git a/crates/vihaco-syntax/src/types.rs b/crates/vihaco-syntax/src/types.rs index 9585cd36..1e0180eb 100644 --- a/crates/vihaco-syntax/src/types.rs +++ b/crates/vihaco-syntax/src/types.rs @@ -28,6 +28,23 @@ where /// The parsed source header. This is distinct from installed runtime metadata. pub header: S::Header, pub functions: Vec>, + pub labels: Vec, + pub constants: Vec, + pub strings: Vec, + pub source_symbols: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedLabel { + pub name: Ident, + pub function: Ident, + pub instruction: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedSourceSymbol { + pub name: Ident, + pub index: u32, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/vihaco/tests/generated_sst_loading.rs b/crates/vihaco/tests/generated_sst_loading.rs index 915c66a8..458633e4 100644 --- a/crates/vihaco/tests/generated_sst_loading.rs +++ b/crates/vihaco/tests/generated_sst_loading.rs @@ -5,7 +5,7 @@ use eyre::Result; use vihaco::{ ContextHandle, Effects, Execute, Execution, LoadSstProgram, LoadSstSubtree, NoEffect, NoMessage, ProgramImage, SstFile, SstGlobalContext, SstSectionView, StepResult, Type, Value, - syntax::{Param, ParsedFunction, ParsedModule}, + syntax::{Param, ParsedFunction, ParsedLabel, ParsedModule, ParsedSourceSymbol}, }; use vihaco_parser::Ident; @@ -87,6 +87,8 @@ vihaco::composite! { Run => runtime Run; #[pattern = "'test::burst $0"] Burst(u32) => lower_burst; + #[pattern = "'test::fail $0"] + Fail(u32) => lower_fail; } runtime { @@ -108,6 +110,13 @@ impl test_machine::syntax::Resolver for TestMachine { .map(|_| TestMachineInstruction::Burst(RuntimeInstruction)) .collect()) } + + fn lower_fail( + &mut self, + count: u32, + ) -> std::result::Result, eyre::Report> { + Err(eyre::eyre!("cannot lower test value {count}")) + } } impl LoadSstProgram for TestMachine { @@ -183,12 +192,33 @@ fn generated_load_parsed_installs_the_complete_module_dialect() { return_ty: None, body: vec![test_machine::syntax::Instruction::Run], }], + labels: vec![ParsedLabel { + name: Ident("entry".to_owned()), + function: Ident("main".to_owned()), + instruction: 0, + }], + constants: vec![Value::I64(7)], + strings: vec!["extra-string".to_owned()], + source_symbols: vec![ParsedSourceSymbol { + name: Ident("source-entry".to_owned()), + index: 4, + }], }; machine.load_parsed(parsed, context).unwrap(); assert_eq!(machine.program.module.code.len(), 1); assert!(machine.program.context.is_some()); + assert_eq!(machine.program.module.labels[0].address, 0); + assert_eq!(machine.program.module.constants, vec![Value::I64(7)]); + assert!( + machine + .program + .module + .strings + .contains(&"extra-string".to_owned()) + ); + assert_eq!(machine.program.module.source_symbols[0].index, 4); } #[test] @@ -200,6 +230,11 @@ fn generated_load_source_expands_one_surface_instruction_to_many() { \t\t\ttest::burst 3\n\ \t\t}\n\ \t.text(root).\n\ +\t.section(child):\n\ +\t\t.text(child):\n\ +\t\t\tchild payload\n\ +\t\t.text(child).\n\ +\t.section(child).\n\ .section(root).\n", ); let mut machine = TestMachine::default(); @@ -230,3 +265,246 @@ fn malformed_root_source_is_rejected_before_program_installation() { assert!(machine.program.context.is_none()); assert_eq!(machine.child.loaded_sst, None); } + +mod nested_loading { + use super::*; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ChildRuntimeInstruction; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct ParentRuntimeInstruction; + + impl Execute for TestComponent { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &ChildRuntimeInstruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } + } + + impl Execute for TestComponent { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &ParentRuntimeInstruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } + } + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ChildHeader; + + impl vihaco::FromText for ChildHeader { + fn from_text(text: &str) -> Result { + (text.trim() == "child-header") + .then_some(Self) + .ok_or_else(|| eyre::eyre!("expected child-header")) + } + } + + impl vihaco::SstHeader for ChildHeader {} + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ParentHeader; + + impl vihaco::FromText for ParentHeader { + fn from_text(text: &str) -> Result { + (text.trim() == "parent-header") + .then_some(Self) + .ok_or_else(|| eyre::eyre!("expected parent-header")) + } + } + + impl vihaco::SstHeader for ParentHeader {} + + mod child_def { + use super::*; + + vihaco::composite! { + #[derive(Default)] + #[allow(dead_code)] + pub composite ChildMachine { + error = eyre::Report; + + #[device(0x11)] + component: TestComponent, + + #[program] + pub program: ProgramImage, + } + + syntax { + header ChildHeader => resolve_header; + #[pattern = "'child::run"] + Run => runtime Run; + } + + runtime { + Run(ChildRuntimeInstruction) => component { + message none; + } + } + } + + impl From for Type { + fn from(value: child_machine::syntax::Type) -> Self { + match value {} + } + } + + impl child_machine::syntax::Resolver for ChildMachine { + fn resolve_header(&mut self, _header: ChildHeader) -> Result<(), eyre::Report> { + Ok(()) + } + } + + impl LoadSstProgram for ChildMachine { + fn load_sst_program<'src>( + &mut self, + section: SstSectionView<'src, TestContext>, + ) -> Result<()> { + self.load_source(section) + } + } + } + + mod parent_def { + use super::child_def::ChildMachine; + use super::*; + + vihaco::composite! { + #[derive(Default)] + #[allow(dead_code)] + pub composite ParentMachine { + error = eyre::Report; + + #[device(0x21)] + component: TestComponent, + + #[program] + pub program: ProgramImage, + + #[device(0x22)] + #[loadable("child")] + pub child: ChildMachine, + } + + syntax { + header ParentHeader => resolve_header; + #[pattern = "'parent::run"] + Run => runtime Run; + } + + runtime { + Run(ParentRuntimeInstruction) => component { + message none; + } + } + } + + impl From for Type { + fn from(value: parent_machine::syntax::Type) -> Self { + match value {} + } + } + + impl parent_machine::syntax::Resolver for ParentMachine { + fn resolve_header(&mut self, _header: ParentHeader) -> Result<(), eyre::Report> { + Ok(()) + } + } + + impl LoadSstProgram for ParentMachine { + fn load_sst_program<'src>( + &mut self, + section: SstSectionView<'src, TestContext>, + ) -> Result<()> { + self.load_source(section) + } + } + } + + fn nested_file(child_header: &str) -> SstFile { + root_file(&format!( + ".section(root):\n\t.header(root):\n\t\tparent-header\n\t.header(root).\n\ +\t.text(root):\n\ +\t\tfn @main() {{\n\ +\t\t\tparent::run\n\ +\t\t}}\n\ +\t.text(root).\n\ +\t.section(child):\n\ +\t\t.header(child):\n\t\t\t{child_header}\n\t\t.header(child).\n\ +\t\t.text(child):\n\ +\t\t\tfn @main() {{\n\ +\t\t\t\tchild::run\n\ +\t\t\t}}\n\ +\t\t.text(child).\n\ +\t.section(child).\n\ +.section(root).\n" + )) + } + + #[test] + fn nested_composites_load_independent_dialects_after_parent_acceptance() { + let file = nested_file("child-header"); + let mut machine = parent_def::ParentMachine::default(); + + machine.load_sst_subtree(file.root()).unwrap(); + + assert_eq!(machine.program.module.code.len(), 1); + assert_eq!(machine.child.program.module.code.len(), 1); + } + + #[test] + fn nested_child_failure_does_not_install_parent_program() { + let file = nested_file("wrong-child-header"); + let mut machine = parent_def::ParentMachine::default(); + + let error = machine.load_sst_subtree(file.root()).unwrap_err(); + + assert!(error.to_string().contains("child")); + assert!(machine.program.module.code.is_empty()); + assert!(machine.program.context.is_none()); + } +} + +#[test] +fn lowerer_errors_identify_function_instruction_and_surface_value() { + let file = root_file( + ".section(root):\n\ +\t.text(root):\n\ +\t\tfn @entry() {\n\ +\t\t\ttest::fail 17\n\ +\t\t}\n\ +\t.text(root).\n\ +.section(root).\n", + ); + let mut machine = TestMachine::default(); + + let error = machine.load_source(file.root()).unwrap_err().to_string(); + + assert!(error.contains("function `entry`"), "{error}"); + assert!(error.contains("instruction 0"), "{error}"); + assert!(error.contains("Fail(17)"), "{error}"); + assert!(error.contains("cannot lower test value 17"), "{error}"); + assert!(machine.program.module.code.is_empty()); + assert!(machine.program.context.is_none()); +} diff --git a/crates/vihaco/tests/module_syntax_regressions.rs b/crates/vihaco/tests/module_syntax_regressions.rs new file mode 100644 index 00000000..b26e67b4 --- /dev/null +++ b/crates/vihaco/tests/module_syntax_regressions.rs @@ -0,0 +1,345 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use chumsky::Parser as _; +use eyre::Result; +use vihaco::{ + BuildProgramModule, ContextHandle, Execute, Execution, FromText, InstallProgramModule, + InstructionSet, ModuleSyntax, NoEffect, NoMessage, Parse, ProgramImage, SstFile, + SstGlobalContext, SstHeader, SstSectionView, StepResult, Type, Value, composite, + loader::{LoadSstProgram, LoadSstSubtree}, + module::{FunctionInfo, LabelInfo, LocalModule, SourceSymbolInfo}, + syntax::{ParsedFunction, ParsedModule, Resolve}, +}; +use vihaco_parser::Ident; + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(instruction)] +enum LocalInstruction { + #[pattern = "'local::run"] + Run, +} + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(type)] +enum LocalType { + #[pattern = "`unit`"] + Unit, +} + +#[derive(Clone, Debug, PartialEq, Parse)] +#[syntax_class(value)] +enum LocalValue { + #[pattern = "`zero`"] + Zero, +} + +struct LocalInstructionSet; + +impl InstructionSet for LocalInstructionSet { + type Instruction = LocalInstruction; + type Value = LocalValue; + type Type = LocalType; +} + +#[derive(Clone, Debug, PartialEq)] +pub struct LocalHeader(String); + +impl FromText for LocalHeader { + fn from_text(text: &str) -> Result { + Ok(Self(text.trim().to_owned())) + } +} + +impl SstHeader for LocalHeader {} + +struct LocalSyntax; + +impl ModuleSyntax for LocalSyntax { + type Instruction = LocalInstruction; + type Value = LocalValue; + type Type = LocalType; + type Header = LocalHeader; +} + +#[derive(Default)] +struct ResolverProbe { + header: Option, +} + +impl Resolve for ResolverProbe { + type Module = ParsedModule; + + fn resolve_module(&mut self, parsed: ParsedModule) -> Result { + self.header = Some(parsed.header.clone()); + Ok(parsed) + } +} + +#[test] +fn resolve_receives_the_complete_parsed_module_and_header() { + let parsed = ParsedModule { + header: LocalHeader("machine-config".to_owned()), + functions: vec![ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::new(), + return_ty: Some(LocalType::Unit), + body: vec![LocalInstruction::Run], + }], + labels: Vec::new(), + constants: Vec::new(), + strings: Vec::new(), + source_symbols: Vec::new(), + }; + let mut resolver = ResolverProbe::default(); + + let resolved = resolver.resolve_module(parsed).unwrap(); + + assert_eq!( + resolver.header, + Some(LocalHeader("machine-config".to_owned())) + ); + assert_eq!(resolved.functions[0].body, vec![LocalInstruction::Run]); + assert_eq!(resolved.functions[0].return_ty, Some(LocalType::Unit)); +} + +#[test] +fn component_instruction_set_is_independent_of_mounting() { + fn require_instruction_set() {} + fn require_surface() {} + + require_instruction_set::(); + require_surface::(); + assert_eq!( + LocalInstruction::parser().parse("local::run").into_result(), + Ok(LocalInstruction::Run) + ); + assert_eq!( + LocalValue::parser().parse("zero").into_result(), + Ok(LocalValue::Zero) + ); + assert_eq!( + LocalType::parser().parse("unit").into_result(), + Ok(LocalType::Unit) + ); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RuntimeInstruction; + +#[derive(Debug, Default)] +struct RuntimeComponent; + +impl Execute for RuntimeComponent { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &RuntimeInstruction, + _message: Self::Message, + ) -> Result, Self::Fault> { + Ok(StepResult { + effects: vihaco::Effects::none(), + execution: Execution::Complete, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TestContext(String); + +impl SstGlobalContext for TestContext { + fn from_text(text: &str) -> Result { + Ok(Self(text.trim().to_owned())) + } +} + +#[derive(Default)] +struct MinimalProgram { + module: Option>, + context: Option>, +} + +impl From for Type { + fn from(value: custom_machine::syntax::Type) -> Self { + match value {} + } +} + +impl BuildProgramModule for MinimalProgram { + type Instruction = CustomMachineInstruction; + type Value = Value; + type Type = Type; + type Info = vihaco::module::NoInfo; + type Module = LocalModule; + + fn empty_module() -> Self::Module { + LocalModule::default() + } + + fn append_instructions( + module: &mut Self::Module, + instructions: impl IntoIterator, + ) { + module.code.extend(instructions); + } + + fn instruction_count(module: &Self::Module) -> u32 { + module.code.len() as u32 + } + + fn add_function(module: &mut Self::Module, function: FunctionInfo) { + module.functions.push(function); + } + + fn add_label(module: &mut Self::Module, label: LabelInfo) { + module.labels.push(label); + } + + fn add_source_symbol(module: &mut Self::Module, symbol: SourceSymbolInfo) { + module.source_symbols.push(symbol); + } + + fn intern_string(module: &mut Self::Module, value: String) -> u32 { + if let Some(index) = module.strings.iter().position(|item| item == &value) { + index as u32 + } else { + let index = module.strings.len() as u32; + module.strings.push(value); + index + } + } + + fn add_constant(module: &mut Self::Module, value: Self::Value) -> u32 { + let index = module.constants.len() as u32; + module.constants.push(value); + index + } + + fn set_main_function(module: &mut Self::Module, function: Option) { + module.main_function = function; + } + + fn finish(module: Self::Module) -> Result { + Ok(module) + } +} + +impl InstallProgramModule for MinimalProgram { + type Module = LocalModule; + + fn install_program_module( + &mut self, + module: Self::Module, + context: ContextHandle, + ) -> Result<()> { + self.module = Some(module); + self.context = Some(context); + Ok(()) + } +} + +composite! { + #[derive(Default)] + #[allow(dead_code)] + composite CustomMachine { + error = eyre::Report; + + #[device(0x01)] + component: RuntimeComponent, + + #[program] + program: MinimalProgram, + } + + syntax { + header LocalHeader => resolve_header; + #[pattern = "'custom::run"] + Run => runtime Run; + } + + runtime { + Run(RuntimeInstruction) => component { + message none; + } + } +} + +impl custom_machine::syntax::Resolver for CustomMachine { + fn resolve_header(&mut self, header: LocalHeader) -> Result<(), eyre::Report> { + if header.0 == "reject" { + Err(eyre::eyre!("rejected custom header")) + } else { + Ok(()) + } + } +} + +impl LoadSstProgram for CustomMachine { + fn load_sst_program<'src>(&mut self, section: SstSectionView<'src, TestContext>) -> Result<()> { + self.load_source(section) + } +} + +fn custom_file(header: &str) -> SstFile { + SstFile::from_text(&format!( + "sst v1\n\n.global:\ncontext\n.global.\n\n.section(root):\n\t.header(root):\n\t\t{header}\n\t.header(root).\n\t.text(root):\n\t\tfn @main() {{\n\t\t\tcustom::run\n\t\t}}\n\t.text(root).\n.section(root).\n" + )) + .expect("test SST should parse") +} + +#[test] +fn generated_loader_uses_minimal_builder_and_installs_expanded_program() { + let file = custom_file("accepted"); + let context = file.context_handle(); + let mut machine = CustomMachine::default(); + + machine.load_sst_subtree(file.root()).unwrap(); + + let program = machine.program.module.as_ref().unwrap(); + assert_eq!(program.code.len(), 1); + assert_eq!(program.functions[0].start_address, 0); + assert_eq!(program.functions[0].end_address, 1); + assert!(machine.program.context.as_ref().unwrap().ptr_eq(&context)); +} + +#[test] +fn rejected_composite_header_does_not_install_partial_program() { + let file = custom_file("reject"); + let mut machine = CustomMachine::default(); + + let error = machine.load_sst_subtree(file.root()).unwrap_err(); + + assert!(error.to_string().contains("rejected custom header")); + assert!(machine.program.module.is_none()); + assert!(machine.program.context.is_none()); +} + +#[test] +fn generated_sums_expose_instruction_value_type_and_alias_contracts() { + fn require_module_syntax() {} + require_module_syntax::(); + + let instruction = custom_machine::syntax::Instruction::parser() + .parse("custom::run") + .into_result() + .unwrap(); + assert!(matches!( + instruction, + custom_machine::syntax::Instruction::Run + )); +} + +// Keep the standard implementation in this integration crate's dependency graph so this +// fixture also guards that runtime-only and syntax-bearing components remain composable. +#[allow(dead_code)] +fn require_standard_program_image() { + fn require_image() + where + T: Default, + { + } + require_image::>(); +} diff --git a/crates/vihaco/tests/multi_route_composite.rs b/crates/vihaco/tests/multi_route_composite.rs new file mode 100644 index 00000000..18934705 --- /dev/null +++ b/crates/vihaco/tests/multi_route_composite.rs @@ -0,0 +1,263 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use chumsky::Parser as _; +use eyre::Result; +use vihaco::{ + ContextHandle, Effects, Execute, Execution, NoEffect, NoMessage, Parse, ProgramImage, + StepResult, Type, Value, component, composite, + syntax::{ParsedFunction, ParsedModule}, +}; +use vihaco_parser::Ident; + +component! { + component Arithmetic {} + + instruction { + #[derive(Clone)] + Add(super::syntax::ArithmeticType), + } + + syntax { + type ArithmeticType { + Integer = "`integer`"; + Address = "`address`"; + Invalid = "`invalid`"; + } + + value ArithmeticValue { + Zero = "`zero`"; + } + + instruction { + Add(ArithmeticType) = "'add $0"; + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for arithmetic::Arithmetic { + fn default() -> Self { + Self {} + } +} + +#[derive(Default)] +struct IntegerStack { + calls: usize, +} + +#[derive(Default)] +struct AddressStack { + calls: usize, +} + +impl Execute for IntegerStack { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &arithmetic::instruction::Add, + _message: Self::Message, + ) -> Result, Self::Fault> { + self.calls += 1; + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } +} + +impl Execute for AddressStack { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + _instruction: &arithmetic::instruction::Add, + _message: Self::Message, + ) -> Result, Self::Fault> { + self.calls += 1; + Ok(StepResult { + effects: Effects::none(), + execution: Execution::Complete, + }) + } +} + +composite! { + #[derive(Default)] + #[allow(dead_code)] + composite MultiRouteMachine { + error = eyre::Report; + + #[syntax("arithmetic")] + arithmetic: arithmetic::Arithmetic, + + integer_stack: IntegerStack, + address_stack: AddressStack, + + #[program] + program: ProgramImage, + } + + runtime { + IntegerAdd(arithmetic::instruction::Add) => integer_stack { + message none; + } + + AddressAdd(arithmetic::instruction::Add) => address_stack { + message none; + } + } +} + +#[derive(Debug)] +struct TestContext; + +impl vihaco::SstGlobalContext for TestContext { + fn from_text(_text: &str) -> Result { + Ok(Self) + } +} + +impl From for Type { + fn from(value: multi_route_machine::syntax::Type) -> Self { + match value { + multi_route_machine::syntax::Type::Arithmetic( + arithmetic::syntax::ArithmeticType::Integer, + ) => Type::I64, + multi_route_machine::syntax::Type::Arithmetic( + arithmetic::syntax::ArithmeticType::Address, + ) => Type::U64, + multi_route_machine::syntax::Type::Arithmetic( + arithmetic::syntax::ArithmeticType::Invalid, + ) => Type::Undefined, + } + } +} + +impl multi_route_machine::syntax::Resolver for MultiRouteMachine { + fn lower_arithmetic( + &mut self, + instruction: arithmetic::syntax::Instruction, + ) -> Result, eyre::Report> { + let runtime_instruction = match instruction { + arithmetic::syntax::Instruction::Add(kind) => arithmetic::instruction::Add(kind), + }; + + match runtime_instruction.0 { + arithmetic::syntax::ArithmeticType::Integer => { + Ok(vec![multi_route_machine::runtime::Instruction::IntegerAdd( + runtime_instruction, + )]) + } + arithmetic::syntax::ArithmeticType::Address => { + Ok(vec![multi_route_machine::runtime::Instruction::AddressAdd( + runtime_instruction, + )]) + } + arithmetic::syntax::ArithmeticType::Invalid => Err(eyre::eyre!( + "arithmetic::add requires a supported source type" + )), + } + } +} + +#[test] +fn component_surface_instruction_selects_runtime_route() { + let integer = multi_route_machine::syntax::Instruction::parser() + .parse("arithmetic::add integer") + .into_result() + .unwrap(); + let address = multi_route_machine::syntax::Instruction::parser() + .parse("arithmetic::add address") + .into_result() + .unwrap(); + + assert!(matches!( + integer, + multi_route_machine::syntax::Instruction::Arithmetic(arithmetic::syntax::Instruction::Add( + arithmetic::syntax::ArithmeticType::Integer + )) + )); + assert!(matches!( + address, + multi_route_machine::syntax::Instruction::Arithmetic(arithmetic::syntax::Instruction::Add( + arithmetic::syntax::ArithmeticType::Address + )) + )); + + let mut machine = MultiRouteMachine::default(); + machine + .load_parsed( + ParsedModule { + header: multi_route_machine::syntax::Header, + functions: vec![ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::>::new( + ), + return_ty: None, + body: vec![integer, address], + }], + labels: Vec::new(), + constants: Vec::new(), + strings: Vec::new(), + source_symbols: Vec::new(), + }, + ContextHandle::new(TestContext), + ) + .unwrap(); + + assert!(matches!( + &machine.program.module.code[..], + [ + MultiRouteMachineInstruction::IntegerAdd(_), + MultiRouteMachineInstruction::AddressAdd(_), + ] + )); + + let integer_instruction = machine.program.module.code[0].clone(); + let address_instruction = machine.program.module.code[1].clone(); + machine.execute_generated(&integer_instruction).unwrap(); + machine.execute_generated(&address_instruction).unwrap(); + + assert_eq!(machine.integer_stack.calls, 1); + assert_eq!(machine.address_stack.calls, 1); +} + +#[test] +fn semantic_type_mismatch_does_not_install_a_program() { + let instruction = multi_route_machine::syntax::Instruction::parser() + .parse("arithmetic::add invalid") + .into_result() + .unwrap(); + let mut machine = MultiRouteMachine::default(); + + let error = machine + .load_parsed( + ParsedModule { + header: multi_route_machine::syntax::Header, + functions: vec![ParsedFunction { + name: Ident("main".to_owned()), + params: Vec::>::new( + ), + return_ty: None, + body: vec![instruction], + }], + labels: Vec::new(), + constants: Vec::new(), + strings: Vec::new(), + source_symbols: Vec::new(), + }, + ContextHandle::new(TestContext), + ) + .unwrap_err(); + + assert!(error.to_string().contains("supported source type")); + assert!(machine.program.module.code.is_empty()); + assert!(machine.program.context.is_none()); +} diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index b97f34cc..4e976e82 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -162,6 +162,10 @@ fn generated_program_loader_builds_and_installs_module() { return_ty: None, body: vec![test_machine::syntax::Instruction::Run], }], + labels: Vec::new(), + constants: Vec::new(), + strings: Vec::new(), + source_symbols: Vec::new(), }; let mut machine = TestMachine { component: TestComponent { diff --git a/demos/examples/demo.md b/demos/examples/demo.md index 68016d3f..f8f86b18 100644 --- a/demos/examples/demo.md +++ b/demos/examples/demo.md @@ -46,7 +46,7 @@ HeterogeneousMachine │ ├── ArithmeticUnit │ ├── ChannelEndpoint> endpoint 0 │ ├── DebugTrace -│ └── program and pc +│ └── SST-loaded program and pc └── Cpu B ├── Stack ├── ArithmeticUnit @@ -79,22 +79,11 @@ its timing ratio; the root looks up the ratio for the selected instance and pass `step_at`, `resume`, and `next_boundary_at`. This keeps timing instance-specific without making it part of the reusable CPU's state. -## Surface and runtime programs +## SST and runtime programs -The surface model is the small `SurfaceInstruction` enum: - -```rust -enum SurfaceInstruction { - Add, - Sub, - Mul, - Send(&'static str), - Recv(&'static str), -} -``` - -`resolve_program` lowers it to `RuntimeInstruction`. Arithmetic becomes a zero-sized runtime -payload (`Add`, `Sub`, or `Mul`); channel names become `ChannelId` values: +Arithmetic and channel components own their local syntax. The composite mounts those syntax sets +under the `arithmetic` and `channel` namespaces, then resolves the parsed component instructions +into runtime route products. Channel names become `ChannelId` values: ```text to_b | from_a -> ChannelId(0) // A to B @@ -115,9 +104,9 @@ resolve_program(&[Recv("from_b"), Mul]); resolve_program(&[Sub, Mul, Send("to_a")]); ``` -There is no parser or module loader in this example yet. The surface values are authored directly, -and resolution is a direct Rust function that demonstrates the required symbolic-to-runtime -boundary. +Both CPU programs are now loaded from SST sections through the generated composite loader. Header +resolution, component syntax parsing, lowering, program installation, and debug-section forwarding +all happen before the event loop starts. ## Components and routes diff --git a/demos/examples/demo.rs b/demos/examples/demo.rs index f4179b6b..26bef428 100644 --- a/demos/examples/demo.rs +++ b/demos/examples/demo.rs @@ -10,7 +10,7 @@ #![allow(dead_code)] use std::collections::HashMap; -use vihaco::Effects; +use vihaco::{Effects, LoadSstSubtree, NoContext, ProgramImage, SstFile, VERSION}; #[path = "demo/stdlib/arithmetic.rs"] mod arithmetic; @@ -42,9 +42,6 @@ mod route; mod stack; #[path = "demo/vihaco/supply.rs"] mod supply; -#[path = "demo/src/surface.rs"] -mod surface; - use arithmetic::ArithmeticUnit; use channel::{ChannelEndpoint, ChannelFabric, EndpointId, SharedTransport}; use clock::{GlobalClock, GlobalTicksPerLocalCycle}; @@ -52,18 +49,7 @@ use cpu::{Cpu, CpuFault}; use debug_trace::DebugTrace; use machine::{CpuId, HeterogeneousMachine, RunOutcome}; use stack::Stack; -use surface::{SurfaceInstruction, resolve_program}; - fn main() -> Result<(), CpuFault> { - // The two CPU programs, authored with symbolic channel names, then resolved to runtime form. - let cpu_a_program = - resolve_program(&[SurfaceInstruction::Recv("from_b"), SurfaceInstruction::Mul]); - let cpu_b_program = resolve_program(&[ - SurfaceInstruction::Sub, - SurfaceInstruction::Mul, - SurfaceInstruction::Send("to_a"), - ]); - // Two instances of the same reusable `Cpu`. Their local-to-global ratios are owned by the // root machine and selected by CpuId when each child is stepped. let fabric = std::rc::Rc::new(std::cell::RefCell::new( @@ -72,25 +58,18 @@ fn main() -> Result<(), CpuFault> { let transport_a = SharedTransport::new(fabric.clone()); let transport_b = SharedTransport::new(fabric.clone()); - let cpu_a = Cpu { - // CpuA starts with a receive and therefore parks at global tick 0. The value sent by - // CpuB becomes the second operand for its multiplication. - operand_stack: Stack::seeded(&[3]), - alu: ArithmeticUnit::new(), - channel: ChannelEndpoint::new(EndpointId(0), transport_a), - debug: DebugTrace::new(), - program: cpu_a_program, - pc: 0, - }; - let cpu_b = Cpu { - // CpuB performs subtraction and multiplication before it reaches the send. - operand_stack: Stack::seeded(&[10, 4, 2]), - alu: ArithmeticUnit::new(), - channel: ChannelEndpoint::new(EndpointId(1), transport_b), - debug: DebugTrace::new(), - program: cpu_b_program, - pc: 0, - }; + let cpu_a = load_cpu( + "channel::recv from_b\narithmetic::mul", + Stack::seeded(&[3]), + EndpointId(0), + transport_a, + )?; + let cpu_b = load_cpu( + "arithmetic::sub\narithmetic::mul\nchannel::send to_a", + Stack::seeded(&[10, 4, 2]), + EndpointId(1), + transport_b, + )?; let mut machine = HeterogeneousMachine { clock: GlobalClock::new(), @@ -140,3 +119,30 @@ fn main() -> Result<(), CpuFault> { println!("OK: heterogeneous exchange completed with 60 on CpuA, no stale continuation"); Ok(()) } + +fn load_cpu( + body: &str, + operand_stack: Stack, + endpoint: EndpointId, + transport: SharedTransport, +) -> Result { + let source = format!( + "sst v{VERSION}\n\n.global:\n.global.\n\n.section(root):\n\t.header(root):\n\t\tdemo-cpu\n\t.header(root).\n\t.text(root):\n\t\tfn @main() {{\n{body}\t\t}}\n\t.text(root).\n\t.section(debug):\n\t\t.text(debug):\n\t\t\tloaded by generated forwarding\n\t\t.text(debug).\n\t.section(debug).\n.section(root).\n", + body = body + .lines() + .map(|line| format!("\t\t\t{line}\n")) + .collect::() + ); + let file = SstFile::::from_text(&source).map_err(CpuFault::Loading)?; + let mut cpu = Cpu { + operand_stack, + alu: ArithmeticUnit::new(), + channel: ChannelEndpoint::new(endpoint, transport), + debug: DebugTrace::new(), + program: ProgramImage::new(), + header: None, + }; + cpu.load_sst_subtree(file.root()) + .map_err(CpuFault::Loading)?; + Ok(cpu) +} diff --git a/demos/examples/demo/src/cpu.rs b/demos/examples/demo/src/cpu.rs index 12d6899a..1dd75649 100644 --- a/demos/examples/demo/src/cpu.rs +++ b/demos/examples/demo/src/cpu.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: MIT use super::{ - arithmetic::{Add, ArithmeticUnit, Mul, Sub}, + arithmetic::{Add, ArithmeticUnit, Mul, Sub, syntax as arithmetic_syntax}, channel::{ - ChannelEndpoint, ReceiveCompletion, ReceiveContinuation, ReceiveEffect, Recv, Send, - SendEffect, SharedTransport, + CHANNEL_A_TO_B, CHANNEL_B_TO_A, ChannelEndpoint, ReceiveCompletion, ReceiveContinuation, + ReceiveEffect, Recv, Send, SendEffect, SharedTransport, syntax as channel_syntax, }, clock::{ ClockFault, ClockedComponent, GlobalTick, GlobalTicksPerLocalCycle, LocalCycles, Schedule, @@ -17,17 +17,42 @@ use super::{ resume::Resume, stack::{Stack, StackFault}, }; +use std::convert::From; + +#[derive(Clone, Debug, PartialEq)] +pub struct CpuHeader(pub String); + +impl vihaco::FromText for CpuHeader { + fn from_text(text: &str) -> eyre::Result { + Ok(Self(text.trim().to_owned())) + } +} + +impl vihaco::SstHeader for CpuHeader {} vihaco::composite! { pub composite Cpu { error = CpuFault; pub operand_stack: Stack, + #[syntax("arithmetic")] pub alu: ArithmeticUnit, + + #[syntax("channel")] pub channel: ChannelEndpoint>, + + pub header: Option, + + #[device(0x01)] + #[loadable] pub debug: DebugTrace, - pub program: Vec, - pub pc: usize, + + #[program] + pub program: vihaco::ProgramImage, + } + + syntax { + header CpuHeader => resolve_header; } runtime { @@ -67,10 +92,92 @@ vihaco::composite! { } } } + } pub type RuntimeInstruction = CpuInstruction; +impl From for vihaco::Type { + fn from(value: cpu::syntax::Type) -> Self { + match value { + cpu::syntax::Type::Alu(arithmetic_syntax::ArithmeticType::Integer) => Self::I64, + cpu::syntax::Type::Channel(channel_syntax::ChannelType::Channel) => Self::U64, + } + } +} + +impl From for vihaco::Value { + fn from(value: cpu::syntax::Value) -> Self { + match value { + cpu::syntax::Value::Alu(arithmetic_syntax::ArithmeticValue::Zero) + | cpu::syntax::Value::Channel(channel_syntax::ChannelValue::ToA) + | cpu::syntax::Value::Channel(channel_syntax::ChannelValue::ToB) + | cpu::syntax::Value::Channel(channel_syntax::ChannelValue::FromA) + | cpu::syntax::Value::Channel(channel_syntax::ChannelValue::FromB) => Self::Undefined, + } + } +} + +impl cpu::syntax::Resolver for Cpu { + fn resolve_header(&mut self, header: CpuHeader) -> Result<(), CpuFault> { + self.header = Some(header); + Ok(()) + } + + fn lower_alu( + &mut self, + instruction: arithmetic_syntax::Instruction, + ) -> Result, CpuFault> { + let instruction = match instruction { + arithmetic_syntax::Instruction::Add => cpu::runtime::Instruction::IntegerAdd(Add), + arithmetic_syntax::Instruction::Sub => cpu::runtime::Instruction::IntegerSub(Sub), + arithmetic_syntax::Instruction::Mul => cpu::runtime::Instruction::IntegerMul(Mul), + }; + Ok(vec![instruction]) + } + + fn lower_channel( + &mut self, + instruction: channel_syntax::Instruction, + ) -> Result, CpuFault> { + let (channel, send) = match instruction { + channel_syntax::Instruction::Send(name) => { + let channel = match name { + channel_syntax::ChannelValue::ToA => CHANNEL_B_TO_A, + channel_syntax::ChannelValue::ToB => CHANNEL_A_TO_B, + channel_syntax::ChannelValue::FromA => CHANNEL_A_TO_B, + channel_syntax::ChannelValue::FromB => CHANNEL_B_TO_A, + }; + (channel, true) + } + channel_syntax::Instruction::Recv(name) => { + let channel = match name { + channel_syntax::ChannelValue::ToA => CHANNEL_B_TO_A, + channel_syntax::ChannelValue::ToB => CHANNEL_A_TO_B, + channel_syntax::ChannelValue::FromA => CHANNEL_A_TO_B, + channel_syntax::ChannelValue::FromB => CHANNEL_B_TO_A, + }; + (channel, false) + } + }; + let instruction = if send { + cpu::runtime::Instruction::Send(Send { channel }) + } else { + cpu::runtime::Instruction::Recv(Recv { channel }) + }; + Ok(vec![instruction]) + } +} + +impl vihaco::loader::LoadSstProgram for Cpu { + fn load_sst_program<'src>( + &mut self, + section: vihaco::SstSectionView<'src, vihaco::NoContext>, + ) -> eyre::Result<()> { + self.load_source(section) + } +} + impl Cpu { fn handle_send(&mut self, effect: SendEffect) -> Result<(), CpuFault> { match effect {} @@ -99,11 +206,15 @@ impl TimedInstruction for RuntimeInstruction { impl Cpu { pub fn fetch(&self) -> Option { - self.program.get(self.pc).cloned() + self.program + .module + .code + .get(self.program.pc as usize) + .cloned() } pub fn finished(&self) -> bool { - self.pc >= self.program.len() && !self.channel.is_parked() + self.program.pc as usize >= self.program.module.code.len() && !self.channel.is_parked() } pub fn is_parked(&self) -> bool { @@ -150,7 +261,7 @@ impl Cpu { ticks_per_local_cycle: GlobalTicksPerLocalCycle, ) -> Result>, CpuFault> { if outcome == Execution::Complete { - self.pc += 1; + self.program.pc += 1; } if outcome == Execution::Parked { @@ -243,11 +354,20 @@ impl ClockedComponent for Cpu { pub enum CpuFault { Stack(StackFault), Clock(ClockFault), + Loading(eyre::Report), UnknownEndpoint, MissingInstruction, MissingTiming, } +impl std::fmt::Display for CpuFault { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "{self:?}") + } +} + +impl std::error::Error for CpuFault {} + impl From for CpuFault { fn from(fault: StackFault) -> Self { CpuFault::Stack(fault) @@ -266,6 +386,12 @@ impl From for CpuFault { } } +impl From for CpuFault { + fn from(fault: eyre::Report) -> Self { + CpuFault::Loading(fault) + } +} + // =========================================================================================== // === END GENERATED ERROR PLUMBING =========================================================== // =========================================================================================== diff --git a/demos/examples/demo/src/driver.rs b/demos/examples/demo/src/driver.rs index 725da6ec..181619f1 100644 --- a/demos/examples/demo/src/driver.rs +++ b/demos/examples/demo/src/driver.rs @@ -5,10 +5,9 @@ mod tests { use crate::{ arithmetic::Add, - channel::Recv, + channel::{CHANNEL_A_TO_B, Recv}, clock::{ClockFault, GlobalTicksPerLocalCycle, LocalCycles, TimedInstruction}, cpu::RuntimeInstruction, - surface::CHANNEL_A_TO_B, }; #[test] diff --git a/demos/examples/demo/src/surface.rs b/demos/examples/demo/src/surface.rs deleted file mode 100644 index 21808819..00000000 --- a/demos/examples/demo/src/surface.rs +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use super::{ - arithmetic::{Add, Mul, Sub}, - channel::{ChannelId, Recv, Send}, - cpu::RuntimeInstruction, -}; - -// =========================================================================================== -// === AUTHOR: surface programs and channel-name resolution ================================== -// =========================================================================================== -// -// The surface form carries symbolic channel names. The machine's resolution step turns each name -// into the library-defined `ChannelId` used by the communication component (requirement 10). - -/// A surface instruction as authored, before channel names are resolved. -#[derive(Debug, Clone, Copy)] -pub enum SurfaceInstruction { - Add, - Sub, - Mul, - Send(&'static str), - Recv(&'static str), -} - -/// The two directed channels wired into this machine. -pub const CHANNEL_A_TO_B: ChannelId = ChannelId(0); -pub const CHANNEL_B_TO_A: ChannelId = ChannelId(1); - -/// Resolve a symbolic channel name to its runtime identifier. `to_b`/`from_a` name the A->B -/// channel; `to_a`/`from_b` name the B->A channel. -pub fn resolve_channel(name: &str) -> ChannelId { - match name { - "to_b" | "from_a" => CHANNEL_A_TO_B, - "to_a" | "from_b" => CHANNEL_B_TO_A, - other => panic!("unknown channel name: {other}"), - } -} - -/// Lower a whole surface program to runtime instructions, resolving channel names along the way. -pub fn resolve_program(surface: &[SurfaceInstruction]) -> Vec { - surface - .iter() - .map(|instruction| match *instruction { - SurfaceInstruction::Add => RuntimeInstruction::IntegerAdd(Add), - SurfaceInstruction::Sub => RuntimeInstruction::IntegerSub(Sub), - SurfaceInstruction::Mul => RuntimeInstruction::IntegerMul(Mul), - SurfaceInstruction::Send(name) => RuntimeInstruction::Send(Send { - channel: resolve_channel(name), - }), - SurfaceInstruction::Recv(name) => RuntimeInstruction::Recv(Recv { - channel: resolve_channel(name), - }), - }) - .collect() -} diff --git a/demos/examples/demo/stdlib/arithmetic.rs b/demos/examples/demo/stdlib/arithmetic.rs index 72254c86..8b4e3eea 100644 --- a/demos/examples/demo/stdlib/arithmetic.rs +++ b/demos/examples/demo/stdlib/arithmetic.rs @@ -21,10 +21,27 @@ vihaco::component! { #[derive(Debug, Clone, Copy)] Mul } + + syntax { + type ArithmeticType { + Integer = "`integer`"; + } + + value ArithmeticValue { + Zero = "`zero`"; + } + + instruction { + Add = "'add"; + Sub = "'sub"; + Mul = "'mul"; + } + } } pub use arithmetic_unit::ArithmeticUnit; pub use arithmetic_unit::instruction::{Add, Mul, Sub}; +pub use arithmetic_unit::syntax; impl ArithmeticUnit { pub fn new() -> Self { diff --git a/demos/examples/demo/stdlib/channel.rs b/demos/examples/demo/stdlib/channel.rs index 38ffecde..15188d26 100644 --- a/demos/examples/demo/stdlib/channel.rs +++ b/demos/examples/demo/stdlib/channel.rs @@ -160,10 +160,32 @@ vihaco::component! { #[derive(Debug, Clone, Copy)] Recv { channel: ChannelId } } + + syntax { + type ChannelType { + Channel = "`channel`"; + } + + value ChannelValue { + ToA = "`to_a`"; + ToB = "`to_b`"; + FromA = "`from_a`"; + FromB = "`from_b`"; + } + + instruction { + Send(ChannelValue) = "'send $0"; + Recv(ChannelValue) = "'recv $0"; + } + } } pub use channel_endpoint::ChannelEndpoint; pub use channel_endpoint::instruction::{Recv, Send}; +pub use channel_endpoint::syntax; + +pub const CHANNEL_A_TO_B: ChannelId = ChannelId(0); +pub const CHANNEL_B_TO_A: ChannelId = ChannelId(1); impl ChannelEndpoint { pub fn new(id: EndpointId, transport: T) -> Self { diff --git a/demos/examples/demo/stdlib/debug_trace.rs b/demos/examples/demo/stdlib/debug_trace.rs index 6e78e027..6c1fd126 100644 --- a/demos/examples/demo/stdlib/debug_trace.rs +++ b/demos/examples/demo/stdlib/debug_trace.rs @@ -5,10 +5,12 @@ use super::{ Effects, handle::{Absorb, Observe}, }; +use vihaco::{LoadSstSubtree, SstSectionView}; vihaco::component! { component DebugTrace { pub records: Vec, + pub loaded_section: Option, } } @@ -18,6 +20,7 @@ impl debug_trace::DebugTrace { pub fn new() -> Self { Self { records: Vec::new(), + loaded_section: None, } } @@ -37,6 +40,16 @@ impl debug_trace::DebugTrace { } } +impl LoadSstSubtree for debug_trace::DebugTrace { + fn load_sst_subtree<'src>( + &mut self, + section: SstSectionView<'src, vihaco::NoContext>, + ) -> eyre::Result<()> { + self.loaded_section = Some(section.sst().to_owned()); + Ok(()) + } +} + #[derive(Debug)] pub struct DebugRecord { pub route: &'static str, diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index 6cca2a2a..7484120f 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -102,7 +102,7 @@ Message sources are deliberately explicit: - `message none` passes `NoMessage`. - `message from field` calls `Supply` on that field. -- `message with method` calls a composite method with the instruction payload. +- `message with method` calls the generated message-resolver trait with the instruction payload. Effect handlers are exclusive: @@ -117,7 +117,7 @@ message, observer, and handler failures. `#[device(code, alias = "name")]` contributes device metadata and source-symbol aliases. Codes must be unique. `#[loadable]` marks a device that receives a direct child SST section through the generated loader. A composite that owns -program data implements `LoadOwnSstSection` in ordinary Rust. +program data implements `LoadSstProgram` in ordinary Rust. The composite macro can also declare structural composites with no `runtime` block. Those composites still provide fields, device diff --git a/docs/src/pages/guide/messages.md b/docs/src/pages/guide/messages.md index e6155682..4d998001 100644 --- a/docs/src/pages/guide/messages.md +++ b/docs/src/pages/guide/messages.md @@ -31,7 +31,7 @@ the composite resolves it through one of the route clauses. ```text message none; // passes NoMessage message from operand_stack; // calls Supply -message with resolve_message; // calls a composite method +message with resolve_message; // calls the generated resolver trait ``` `message from field` is useful when a reusable component already knows how to @@ -39,7 +39,7 @@ produce the message. `message with method` is the right boundary when several fields or machine policy must be combined: ```rust ignore -impl Calculator { +impl calculator::runtime::MessageResolver for Calculator { fn resolve_add( &mut self, _instruction: &calculator::instruction::Add, diff --git a/docs/src/pages/guide/parser-advanced.md b/docs/src/pages/guide/parser-advanced.md index aa22f5ca..0dd93cb7 100644 --- a/docs/src/pages/guide/parser-advanced.md +++ b/docs/src/pages/guide/parser-advanced.md @@ -25,30 +25,33 @@ represented explicitly in the syntax type and its patterns. `vihaco::syntax` exposes the typed intermediate representation: ```rust ignore -use vihaco::SurfaceInstruction; +use vihaco::{ModuleSyntax, SurfaceInstruction}; use vihaco_parser::Ident; -pub struct ParsedModule +pub struct ParsedModule where - I: SurfaceInstruction, + S: ModuleSyntax, { - pub header: H, - pub functions: Vec>, + pub header: S::Header, + pub functions: Vec>, } -pub struct ParsedFunction +pub struct ParsedFunction where - I: SurfaceInstruction, + S: ModuleSyntax, { pub name: Ident, - pub params: Vec>, - pub return_ty: Option, - pub body: Vec, + pub params: Vec>, + pub return_ty: Option, + pub body: Vec, } -pub struct Param { +pub struct Param +where + S: ModuleSyntax, +{ pub name: Ident, - pub ty: Ty, + pub ty: S::Type, } ``` @@ -126,8 +129,16 @@ use vihaco::{NoContext, SstFile}; use vihaco::syntax::ParsedModule; let file = SstFile::::from_text(source)?; -let parsed = - ParsedModule::::parse_section(file.root())?; +struct DeviceSyntax; + +impl ModuleSyntax for DeviceSyntax { + type Instruction = DeviceInstruction; + type Value = (); + type Type = DeviceType; + type Header = DeviceHeader; +} + +let parsed = ParsedModule::::parse_section(file.root())?; ``` `parsed.header` is the typed `DeviceHeader`, while each function body contains @@ -135,8 +146,8 @@ only `DeviceInstruction` values and its signature uses `DeviceType`. ## Step 4: resolve into a runtime module -`Resolve` owns the application-specific conversion from a -`ParsedModule` to any output module type. +`Resolve` owns the application-specific conversion from a +`ParsedModule` to any output module type. ```rust ignore use vihaco::module::LocalModule; @@ -146,12 +157,12 @@ use vihaco::{Type, Value}; #[derive(Default)] struct DeviceResolver; -impl Resolve for DeviceResolver { +impl Resolve for DeviceResolver { type Module = LocalModule; fn resolve_module( &mut self, - parsed: ParsedModule, + parsed: ParsedModule, ) -> eyre::Result { let mut module = LocalModule::default(); for function in parsed.functions { @@ -199,7 +210,7 @@ later conversion. A generated composite instruction enum is the runtime dispatch type. SST source is parsed through user-declared surface instruction types, each deriving `Parse` with its own namespace and patterns. Parse each component section as a -`ParsedModule`, resolve it, +`ParsedModule`, resolve it, and load the resulting runtime instructions into that component. This keeps source syntax attached to the component that owns it. Composite diff --git a/vision/composite-syntax-runtime-plan.md b/vision/composite-syntax-runtime-plan.md index 666b94a9..29673112 100644 --- a/vision/composite-syntax-runtime-plan.md +++ b/vision/composite-syntax-runtime-plan.md @@ -3,20 +3,26 @@ ## Status Design plan for the SST-only instruction pipeline. This document defines how a -composite declares source syntax, lowers parsed instructions into runtime -instructions, and executes those instructions through typed component routes. +composite declares a complete module syntax, resolves parsed headers and +instructions into runtime instructions, and executes those instructions +through typed component routes. -Components provide reusable runtime products and `Execute` implementations. -Composites provide the machine-specific SST vocabulary, lowering policy, route -selection, message resolution, and effect handling. +The `ModuleSyntax` refactor and header-resolution design are detailed in +[`module-syntax-plan.md`](module-syntax-plan.md). + +Components provide reusable instruction-set syntax, value/type syntax, runtime +instruction products, and `Execute` implementations. Composites compose those +syntax contributions, own SST section headers, and provide the machine-specific +lowering policy, route selection, message resolution, and effect handling. ## Pipeline ```text SST section -> generated composite surface parser - -> ParsedModule - -> composite syntax-resolver trait + -> ParsedModule + -> parsed-header resolution + -> composite syntax-resolver traits -> Vec -> program-container module installation -> program-counter execution @@ -42,7 +48,8 @@ resolution. An executable composite has three relevant parts: 1. A `#[program]` field that owns the loaded program and program counter. -2. A `syntax` block that defines the composite's public SST vocabulary. +2. A `syntax` block that defines the composite's complete public SST dialect: + surface instructions, function types, and parsed headers. 3. A `runtime` block that defines executable routes. Illustrative shape: @@ -75,25 +82,21 @@ vihaco::composite! { } syntax { - #[pattern = "'processor::step $0"] - Step(StepSyntax) => lower_step; - - #[pattern = "'waveform::play $0"] - Play(PlaySyntax) => lower_play; + header ControlHeaderBlock => resolve_header; - #[pattern = "'optical::clear"] - Clear => runtime Clear; + // Component instruction-set syntax is composed from the device fields. + // Composite-only sugar may also be declared here. } runtime { - Step(processor::instruction::Step) => processor { + ProcessorStep(processor::instruction::Step) => processor { message with resolve_step; effects { handle with handle_step; } } - Play(waveform::instruction::Play) => waveform { + WaveformPlay(waveform::instruction::Play) => waveform { message with resolve_play; effects { observe stdout; @@ -101,7 +104,7 @@ vihaco::composite! { } } - Clear(optical::instruction::Clear) => optical { + OpticalClear(optical::instruction::Clear) => optical { message none; effects { handle with handle_optical; @@ -116,36 +119,68 @@ that syntax names and runtime route names are allowed to differ. ## Generated modules +The complete module source dialect is represented by one syntax type: + +```rust +pub trait ModuleSyntax { + type Instruction: SurfaceInstruction; + type Value; + type Type; + type Header: SstHeader; +} +``` + The composite macro generates namespaced modules rather than placing all products in the composite's parent namespace: ```rust pub mod control_machine { pub mod syntax { + pub struct Module; + pub enum Instruction { - Step(StepSyntax), - Play(PlaySyntax), - Clear, + Processor(processor::syntax::Instruction), + Waveform(waveform::syntax::Instruction), + Optical(optical::syntax::Instruction), + } + + pub enum Value { + Processor(processor::syntax::Value), + Waveform(waveform::syntax::Value), + Optical(optical::syntax::Value), + } + + pub enum Type { + Processor(processor::syntax::Type), + Waveform(waveform::syntax::Type), + Optical(optical::syntax::Type), + } + + impl ::vihaco::ModuleSyntax for Module { + type Instruction = Instruction; + type Value = Value; + type Type = Type; + type Header = ControlHeaderBlock; } pub trait Resolver { - fn lower_step( + fn resolve_header( &mut self, - instruction: StepSyntax, - ) -> Result, ControlMachineFault>; + header: ControlHeaderBlock, + ) -> Result<(), ControlMachineFault>; - fn lower_play( + fn lower_processor( &mut self, - instruction: PlaySyntax, + instruction: processor::syntax::Instruction, ) -> Result, ControlMachineFault>; } } pub mod runtime { pub enum Instruction { - Step(processor::instruction::Step), - Play(waveform::instruction::Play), - Clear(optical::instruction::Clear), + ProcessorStep(processor::instruction::Step), + WaveformPlay(waveform::instruction::Play), + OpticalClear(optical::instruction::Clear), } pub trait MessageResolver { @@ -164,20 +199,23 @@ pub use control_machine::syntax::Resolver as ControlMachineSyntaxResolver; pub use control_machine::runtime::MessageResolver as ControlMachineMessageResolver; ``` -The generated syntax enum implements the parser's surface-instruction marker -and parser interface. The runtime enum is the execution boundary and does not -implement source parsing by default. +The generated syntax enum is a sum over the participating component +instruction sets and implements the parser's surface-instruction marker and +parser interface. The generated `syntax::Type` is the corresponding sum over +component and core source types. The generated `syntax::Module` identifies the +complete source dialect consumed by `ParsedModule`. The runtime enum is the +execution boundary and does not implement source parsing by default. ## Syntax declarations -Composite syntax patterns use complete public spellings. The new pattern -grammar does not require an instruction `head`: +Component instruction sets own local instruction and operand syntax. The +composite composes those sets, adds public namespaces and aliases, and may +declare composite-only sugar. The new pattern grammar does not require an +instruction `head`. ```rust -syntax { - #[pattern = "'waveform::play $0"] - Play(PlaySyntax) => lower_play; -} +processor::syntax::Instruction +waveform::syntax::Instruction ``` Instruction tokens accept namespaced identifiers: @@ -186,93 +224,187 @@ Instruction tokens accept namespaced identifiers: instruction-token = identifier, { "::", identifier } ; ``` -The composite syntax block establishes the instruction syntax class, so -composite-generated instruction enums do not need an explicit -`#[syntax_class(...)]` attribute. User-defined payload types continue to use -the parser derive and syntax classes appropriate to their role. +The composite-generated instruction and type sums do not need explicit user +written `#[derive(Parse)]` declarations. Component-owned syntax types continue +to use the parser derive and syntax classes appropriate to their role. -### User-defined payload syntax +### Component instruction sets -The composite owns the instruction prefix. A payload type owns the grammar of -its operands: +Components may expose optional instruction-set syntax products: ```rust -#[derive(vihaco_parser_derive::Parse)] -#[syntax_class(value)] -#[pattern = "$duration `,` $mode"] -pub struct PlaySyntax { - pub duration: u64, - pub mode: PlayMode, +pub trait InstructionSet { + type Instruction: SurfaceInstruction; + type Value; + type Type; } +``` -vihaco::composite! { - // ... - syntax { - #[pattern = "'waveform::play $0"] - Play(PlaySyntax) => lower_play; +The component owns its local instruction and value/type grammar, as well as +the runtime instruction products that the composite can select. Runtime-only +components do not need to implement `InstructionSet`. + +The composite adds public namespaces and delegates parsing to the selected +component: + +```text +processor::step 100 + -> SurfaceInstruction::Processor( + processor::syntax::Instruction::Step(...) + ) +``` + +The same component syntax may be mounted more than once under different +aliases. Components do not know their device code, mounted alias, parent +composite, or runtime route identity. + +### Component-owned payload syntax + +The component owns the grammar of its operands through the declarative pattern +parser. Values and types are generated enums rather than user-written parser +structs: + +```rust +syntax { + value Value { + U32(u32), + Label(LabelRef), + } + + type Type { + I64 = "`i64`"; + U32 = "`u32`"; + } + + instruction { + Step(value: Value) = "'step $value"; + Branch(target: Value) = "'br $target"; + Add(ty: Type) = "'add $ty"; } } ``` -`$0` invokes `PlaySyntax::parser()`. This keeps nested operand syntax -composable and prevents the composite macro from becoming a second struct -pattern parser. +Primitive parser names such as `u32`, `i64`, and `ident` are built in. Named +values and types can be referenced as nested parsers. The macro generates the +`Parse` implementations using the shared pattern parser; users do not write +Chumsky parsers manually. -### Direct mappings +### Composite-only syntax -Direct mappings are limited initially to unit instructions: +Composite-only syntax is available for machine-level operations that do not +belong to a reusable component: ```rust syntax { + header ControlHeaderBlock => resolve_header; + #[pattern = "'optical::clear"] Clear => runtime Clear; } ``` -The macro constructs the runtime route directly. Argument-bearing instructions -use named lowerers because procedural macros cannot inspect arbitrary external -runtime product definitions and infer safe conversions. +Composite-only instructions are useful for machine-level operations that do +not belong to a reusable component. Argument-bearing instructions use named +lowerers because procedural macros cannot inspect arbitrary external runtime +product definitions and infer safe conversions. -### Delegated syntax +### Component syntax composition -Components do not provide parsers in the initial design. A composite may, -however, explicitly delegate an existing syntax vocabulary in the future or -where a reusable parser type already exists: +The composite parser delegates public namespaced instructions to the syntax +owned by the selected component: ```rust syntax { - #[delegate(host_vm::Instruction, prefix = "processor")] - Processor(host_vm::Instruction) => runtime Processor; + header ControlHeaderBlock => resolve_header; + + #[pattern = "'halt"] + Halt => runtime Halt; } ``` -Delegation imports syntax; it does not make the component's instruction enum -the composite execution boundary. +Component syntax is selected separately, for example with a `#[syntax]` field +attribute carrying the public prefix. The generated composite instruction +variant wraps the component instruction; the runtime route remains a separate +composite decision. ## Syntax resolution -The macro generates a public syntax-resolver trait for named lowerers. The -trait is implemented directly by the composite: +Parsing and resolution are separate. A parsed header is source syntax, not +runtime metadata. The resolver consumes the complete `ParsedModule` and must +resolve the header before the runtime module is installed. This follows the +Acamar model, where a parsed header block is applied to runtime `Info` such as +`DeviceInfo`. + +The macro generates a public syntax-resolver trait for header resolution and +named lowerers. The trait is implemented directly by the composite: ```rust impl ControlMachineSyntaxResolver for ControlMachine { - fn lower_play( + fn resolve_header( &mut self, - instruction: PlaySyntax, + header: ControlHeaderBlock, + ) -> Result<(), ControlMachineFault> { + // Validate and apply parsed header directives. + Ok(()) + } + + fn lower_processor( + &mut self, + instruction: processor::syntax::Instruction, ) -> Result, ControlMachineFault> { - let duration_ns = instruction.duration.try_into()?; + match instruction { + processor::syntax::Instruction::Step(value) => { + let value = match value { + processor::syntax::Value::U32(value) => value, + other => { + return Err(ControlFault::type_error( + "processor::step expects a u32 value", + other, + )); + } + }; + + Ok(vec![RuntimeInstruction::ProcessorStep( + processor::instruction::Step { value }, + )]) + } + processor::syntax::Instruction::Branch(value) => { + let label = match value { + processor::syntax::Value::Label(label) => label, + other => { + return Err(ControlFault::type_error( + "processor::br expects a label", + other, + )); + } + }; + let target = self.program.resolve_label(&label)?; + + Ok(vec![RuntimeInstruction::ProcessorBranch( + processor::instruction::Branch { target }, + )]) + } + processor::syntax::Instruction::Add(ty) => { + let ty = self.program.resolve_type(ty)?; - Ok(vec![RuntimeInstruction::Play( - waveform::instruction::Play { duration_ns }, - )]) + Ok(vec![RuntimeInstruction::ProcessorAdd( + processor::instruction::Add { ty }, + )]) + } + processor::syntax::Instruction::Reset => Ok(vec![ + RuntimeInstruction::ProcessorReset( + processor::instruction::Reset, + ), + ]), + } } } ``` -Lowerers receive only the parsed syntax value. They access module-resolution -state through `self.program` and may use other composite fields when the -machine explicitly permits it. The program object owns the resolution context; -the composite owns the machine-specific lowering policy. +Lowerers receive parsed syntax values and types. They perform semantic analysis +through `self.program`, including label, type, constant, and metadata +resolution. The program object owns the resolution context; the composite +owns the machine-specific lowering policy. Every named lowerer returns an owned sequence: @@ -285,8 +417,14 @@ Module-level resolution assigns final instruction addresses after expansion so labels and source symbols refer to the runtime program rather than the surface instruction sequence. -The generated resolver trait contains only named lowerers. Direct mappings do -not create user methods. +The generated resolver trait contains the declared header resolver and named +lowerers. Direct instruction mappings do not create user methods. + +The exact header-resolver return type remains open. It may mutate composite +state, produce module `Info`, or use an explicit resolution context. If header +results populate the program module's `Info`, `BuildProgramModule` needs an +explicit metadata assignment operation. Header failures use the composite's +declared error type and must be reported before installation. ## Multiple runtime routes @@ -408,8 +546,8 @@ The framework keeps program behavior split across focused traits: ```rust ProgramCounter GetProgramInfo -LoadOwnSstSection -LoadSstSection +LoadSstProgram +LoadSstSubtree InstallProgramModule ``` @@ -437,42 +575,78 @@ Installation replaces the runtime module, context, and PC as one operation. Program-specific lookup APIs remain author-defined. The framework does not require a universal `resolve_string` or `resolve_constant` method. +Parsed headers are resolved before installation. If a composite's resolved +header data belongs in the program module's `Info` value, the builder must +provide an explicit metadata assignment operation, for example: + +```rust +fn set_info(module: &mut Self::Module, info: Self::Info); +``` + +A parsed `SstHeader` is source syntax, not the installed runtime metadata. + ## SST loading -The generated root loading path uses the existing multi-section loading model: +The generated root loading path uses the multi-section loading model: ```text -LoadSstSection(root) - -> generated composite LoadOwnSstSection - -> parse root syntax - -> lower through ControlMachineSyntaxResolver - -> build temporary runtime module - -> InstallProgramModule on #[program] - -> forward direct child sections to #[loadable] fields +root.load_source(...) + -> parse ParsedModule + -> resolve the parsed header + -> lower through the generated syntax resolver + -> resolve module metadata + -> build a temporary runtime module + -> InstallProgramModule on #[program] + -> LoadSstSubtree for each direct child section + +LoadSstSubtree(child) + -> LoadSstProgram for the child composite's own #[program] section + -> LoadSstSubtree for each nested #[loadable] child ``` +`LoadSstProgram` is the capability for loading a composite's own program and +associated SST data. `LoadSstSubtree` is the recursive capability for loading a +complete device section, including the current device and all nested device +sections. Leaf devices implement `LoadSstSubtree` directly; generated +composites use it to load their own program and then forward child sections. + +Each composite owns a generated `ModuleSyntax` containing sums of its selected +component instruction, value, and type syntax plus its composite-owned parsed +header syntax. A nested composite's generated loader uses its own +`ModuleSyntax`; the parent only requires the child's `LoadSstSubtree` +implementation and never names the child's parser types. + The root program is resolved independently of arbitrary live child-device state. Child sections may provide explicit load metadata through the program's resolution context, but syntax lowering does not inspect arbitrary device fields. -The load is transactional. Parsing, lowering, expansion, label assignment, and -module construction complete before the program container replaces its current -module. A failure leaves the previously loaded program intact. +Loading is one-shot. Parsing, lowering, expansion, label assignment, and module +construction must complete before the program container is mutated. Malformed +input returns an error and does not install a partially built program. The +framework does not provide rollback to a previously installed program; these +machines are intended to be loaded once unless a future use case requires +reloading. Generated composite methods should include: ```rust -fn load_source(&mut self, source: &str) -> Result<(), ControlMachineFault>; +fn load_source( + &mut self, + section: SstSectionView<'_, Context>, +) -> eyre::Result<()>; -fn load_parsed( +fn load_parsed( &mut self, - parsed: ParsedModule, -) -> Result<(), ControlMachineFault>; + parsed: ParsedModule, + context: ContextHandle, +) -> eyre::Result<()>; ``` `load_parsed` is the primary unit-testing boundary for lowering. It avoids coupling resolver tests to text parsing or section-container construction. +It must also exercise parsed-header resolution rather than discarding the +header value. SST is the only loading format covered by this design. Bytecode loading is outside the scope of the new pipeline. @@ -516,9 +690,22 @@ the initial design. ## Parser changes -The parser derive and shared parser machinery need the following changes for +The shared syntax layer and parser machinery need the following changes for the new composite model: +- add `ModuleSyntax` as the complete module source-dialect boundary; +- use `ParsedModule` as the complete parsed-module boundary; +- make `ParsedFunction` obtain its instruction and type syntax from the + module dialect; +- update `Resolve` to consume `ParsedModule` and therefore receive + parsed headers; +- keep parsed headers distinct from resolved module metadata; +- add component instruction-set syntax products for surface instructions and + source values and types, generated through the declarative pattern parser; +- generate composite instruction, value, and source-type sums from + participating components; +- keep SST section-header syntax and header resolution on the composite; + - Remove the requirement for instruction `head`. - Support complete namespaced instruction tokens in patterns. - Keep `syntax_class` for standalone user-defined payload types. @@ -526,37 +713,115 @@ the new composite model: without requiring user-written `#[derive(Parse)]` declarations. - Keep payload parsing compositional through each payload type's `Parse` implementation. -- Do not require reusable components to provide parsers. +- Allow reusable components to provide optional instruction-set parsers. The parser derive's existing pattern validation remains valuable: field bindings must be complete, unambiguous, and type-directed. ## Implementation phases -1. Audit `ProgramCounter`, `GetProgramInfo`, `LoadOwnSstSection`, - `LoadSstSection`, and the generated multi-section loading paths. -2. Define and implement `InstallProgramModule` with transactional module, - context, and PC installation. -3. Add composite-generated `syntax` and `runtime` modules. -4. Generate surface instruction enums and parser implementations from - composite syntax entries. -5. Remove instruction `head` requirements and add namespaced pattern tokens. -6. Generate public syntax-resolver and message-resolver traits. -7. Generate SST root loading and `load_parsed` paths for composites with - `#[program]` and `syntax`. -8. Generate runtime route dispatch from the `runtime` block. -9. Migrate a small composite with unit direct mappings and argument-bearing - named lowerers. -10. Migrate a composite with one surface instruction selecting multiple - runtime routes. -11. Add transactional-load, interning, one-to-many expansion, source-location, - and message-resolution tests. +Status markers: **completed** means implemented and verified; **partial** means +the listed sub-items are split between finished and remaining work; **remaining** +means not implemented yet. + +Implementation checkpoint: Steps 1–11 and the core of Step 13 are implemented +and covered by the workspace checks completed on 2026-08-11. The remaining +items below are the deliberate follow-up work, not unreviewed plan items. + +1. **Completed — audit and runtime foundations.** Audit + `ProgramCounter`, `GetProgramInfo`, the loading paths, and generated + multi-section behavior. + +2. **Completed — `InstallProgramModule`.** Install a prevalidated module, + context, and PC state as one operation. One-shot loading semantics are now + intentional; rollback is not provided. + +3. **Completed — generated composite modules.** Generate composite `syntax` + and `runtime` modules. Extend the syntax module with a generated + `syntax::Module` dialect marker and component-contribution sums. + +4. **Completed — generated surface parsers.** Generate surface instruction + enums and parser implementations by composing participating component + instruction-set parsers. + +5. **Completed — parser pattern changes.** Remove instruction `head` + requirements and support namespaced pattern tokens. + +6. **Completed — `ModuleSyntax` source-dialect model.** `ParsedModule`, + `ParsedFunction`, `Param`, and `Resolve` now derive + instruction, value, type, and header syntax from one module dialect. Parsed + headers remain source syntax and are delivered to the resolver. + +7. **Completed — component syntax composition.** `InstructionSet` is optional + for components, runtime-only components remain valid, and generated + composite instruction/value/type sums support public namespaces, aliases, + repeated mounts, and explicit wrapping. Focused component and composite + codegen coverage exists. + +8. **Completed — header resolution boundary; metadata policy intentionally + composite-owned.** Composite-owned header syntax, `parse_header`, and the + generated header resolver are implemented. Headers are resolved before + lowering and installation, and invalid headers cannot partially install a + program. The current policy applies resolved header state through the + composite resolver; `BuildProgramModule::set_info` is not required by the + current implementation. Add it later only if installed `Info` must carry + header metadata. + +9. **Completed — SST program and subtree loading.** Generated loading uses + `ParsedModule`, has no caller-supplied syntax/header + generics, resolves headers before installation, supports one-to-many + lowering, and installs only after validation. `LoadSstProgram` and + `LoadSstSubtree` are the final capability names. Nested composites load + their own program with their own dialect before forwarding descendants; + missing, unexpected, duplicate, and invalid child sections are rejected + without installing the parent program. + +10. **Completed — runtime route dispatch.** Generate runtime route dispatch + from the `runtime` block, including message resolution and effect + handling. + +11. **Completed — simple composite migration.** Migrate and test direct + routes and named lowerers with arguments. Update the migration to use + component instruction-set syntax and the composite-generated module + syntax dialect. + +12. **Completed — multi-route composite migration.** Added a component-owned + arithmetic surface syntax mounted under a composite namespace. A named + component lowerer selects between two runtime routes carrying the same + component instruction product, and integration coverage verifies parsing, + module installation, route selection, and generated dispatch to the two + target components. + +13. **Completed — test coverage.** Coverage now includes: + + - malformed root SST rejection; + - module installation and custom minimal program containers; + - message resolution and runtime-only component composition; + - basic string interning behavior; + - component instruction-set parsing and composite source-sum/alias coverage; + - `ModuleSyntax` parsing and `Resolve` coverage; + - parsed-header resolution, invalid-header diagnostics, and no-partial-install; + - one-to-many lowering and final expanded instruction addresses; + - function/instruction/surface-value lowering diagnostics; + - nested subtree loading with independent child syntax dialects; + - semantic source-type mismatch diagnostics with no partial installation; + - parsed labels, constants, strings, and source symbols carried into the + installed runtime module. + + Resolved header state remains composite-owned, so `Module::Info` metadata + installation is intentionally not part of the current policy. + +14. **Completed — demo migration.** Arithmetic and channel components now own + their local syntax. The demo uses a `ProgramImage`-backed CPU program, + loads both CPU programs through the generated composite dialect and SST, + resolves and installs its composite-owned header, and forwards the debug + child section through `LoadSstSubtree` before execution. ## Non-goals This design does not initially provide: -- component-owned parsers or default component syntax; +- mandatory parsers for components that do not expose source syntax; - bytecode loading; - declarative field-by-field runtime constructors; - a universal program lookup API for strings or constants; diff --git a/vision/execution-pipeline.md b/vision/execution-pipeline.md index 71c5052f..a7467f0b 100644 --- a/vision/execution-pipeline.md +++ b/vision/execution-pipeline.md @@ -11,8 +11,8 @@ SST loading follows this path: ```text SST text -> pattern parser - -> ParsedModule - -> Resolve + -> ParsedModule + -> Resolve -> Module -> runtime program image ``` @@ -26,7 +26,7 @@ the resulting module through `BuildProgramModule` and ```text SST section -> composite::syntax::Instruction parser - -> ParsedModule + -> ParsedModule -> generated composite lowering -> BuildProgramModule -> load_parsed(parsed, ContextHandle) @@ -39,7 +39,7 @@ convenience for executable composites and does not require the runtime instruction enum to implement bytecode encoding. `SurfaceType`, `Constant`, and `RuntimeType` are author-defined products rather than vihaco enums. -`Resolve` owns every transformation that requires +`Resolve` owns every transformation that requires module-wide source context: - Building and consulting label tables. @@ -53,12 +53,12 @@ module-wide source context: At the trait boundary, resolution consumes a parsed surface module and produces a runtime module: ```rust -pub trait Resolve { +pub trait Resolve { type Module; fn resolve_module( &mut self, - parsed: ParsedModule, + parsed: ParsedModule, ) -> eyre::Result; } ``` @@ -137,7 +137,7 @@ rather than universal step behavior. Runtime message resolution supplies the owned, execution-time information that is intentionally absent from the instruction. It is distinct from -`Resolve`: module resolution transforms parsed source +`Resolve`: module resolution transforms parsed source into a runtime program, while message resolution reads live machine state for an instruction that is already fully resolved. @@ -365,7 +365,7 @@ outer machine instruction; it is not resolved globally from `Effect`. The composite declaration defines the available runtime routes, and the composite macro gives each one a machine instruction variant. -`Resolve` selects among those variants +`Resolve` selects among those variants while lowering surface instructions into the runtime module. This separation allows one SST operation to select a machine-specific execution path after its diff --git a/vision/macro-generation.md b/vision/macro-generation.md index 95e57f1c..dd677bc0 100644 --- a/vision/macro-generation.md +++ b/vision/macro-generation.md @@ -69,7 +69,7 @@ The composite/machine macro: - Requires every selected surface instruction to implement `vihaco_parser_core::Parse<'src>`. - Uses the author-selected module surface type for function signatures and declarations. - Generates the runtime instruction sum. -- Requires a `Resolve` implementation whose +- Requires a `Resolve` implementation whose output module uses the machine runtime instruction sum and author-defined constant/type products. - Generates the outer execution match. - Generates or calls route-specific message resolvers. diff --git a/vision/module-syntax-plan-original.md b/vision/module-syntax-plan-original.md new file mode 100644 index 00000000..8fea841b --- /dev/null +++ b/vision/module-syntax-plan-original.md @@ -0,0 +1,511 @@ +# Component Instruction Sets and Composite Module Syntax + +## Purpose + +This document refines the composite syntax/runtime plan around a clearer +ownership boundary: + +```text +component + owns instruction syntax, value/type syntax, and runtime instruction products + +composite + owns SST section headers, namespaces, syntax composition, lowering, and routes +``` + +The source dialect consumed by an SST module is represented by one +`ModuleSyntax` type. Its instruction, value, and source-type parts are generated from +the composite's participating components, while its header part is defined by the +composite that owns the SST section. + +Acamar demonstrates why headers remain composite-owned. Its +`AcamarHeaderBlock` is parsed from the Acamar section and then resolved by +`AcamarResolver` into `DeviceInfo`. It is not syntax owned by the CPU, FPGA, +camera, or other child components. + +## Component instruction sets + +Components may expose an instruction-set syntax product: + +```rust +pub trait InstructionSet { + type Instruction: SurfaceInstruction; + type Value; + type Type; +} +``` + +A component can provide syntax alongside its runtime instruction products: + +```rust +pub mod processor { + pub mod syntax { + pub enum Value { + U32(u32), + Label(LabelRef), + } + + pub enum Instruction { + Step(Value), + Branch(Value), + Add(Type), + Reset, + } + + pub enum Type { + I64, + U32, + } + } + + pub mod instruction { + pub struct Step { + pub duration: u64, + } + + pub struct Reset; + } +} +``` + +The component declaration that produces these syntax types is declarative: + +```rust +syntax { + value LabelRef = "'@' ident"; + + value Value { + U32(u32), + Label(LabelRef), + } + + type Type { + I64 = "`i64`"; + U32 = "`u32`"; + } + + instruction { + Step(value: Value) = "'step $value"; + Branch(target: Value) = "'br $target"; + Add(ty: Type) = "'add $ty"; + Reset = "'reset"; + } +} +``` + +The macro uses the shared pattern parser to generate `Parse` implementations; +component authors do not write Chumsky parsers manually. + +The component owns the grammar and parser implementations for its instruction +and source-type syntax. It does not own: + +- SST section headers; +- device aliases or public namespaces; +- device codes; +- runtime route identity; +- machine-wide metadata; +- composite-specific lowering policy. + +Syntax is optional. Runtime-only components remain valid components without an +`InstructionSet` implementation. + +## Composite-generated source sums + +Given: + +```rust +#[device(0x01, alias = "processor")] +processor: Processor, + +#[device(0x02, alias = "waveform")] +waveform: Waveform, +``` + +the composite generates source sums: + +```rust +pub enum SurfaceInstruction { + Processor(processor::syntax::Instruction), + Waveform(waveform::syntax::Instruction), +} + +pub enum SurfaceValue { + Processor(processor::syntax::Value), + Waveform(waveform::syntax::Value), +} + +pub enum SurfaceType { + Processor(processor::syntax::Type), + Waveform(waveform::syntax::Type), +} +``` + +The generated sum is explicit Rust enum composition, not an implicit union. +The composite must define how duplicate or ambiguous source spellings are +handled. Namespaced type syntax may be required when component type grammars +overlap. + +The composite may also contribute shared/core syntax types if the module +language has types that are not owned by one device: + +```rust +pub enum SurfaceType { + Core(CoreType), + Processor(processor::syntax::Type), + Waveform(waveform::syntax::Type), +} +``` + +## Composite-owned headers + +Headers are defined by the composite syntax declaration because the composite +owns the SST section: + +```rust +syntax { + header ControlHeaderBlock => resolve_header; +} +``` + +The composite may wrap device-specific header fragments, but those fragments +remain part of the composite's header grammar: + +```rust +pub enum ControlHeader { + Processor(ProcessorHeader), + Waveform(WaveformHeader), + Clock(ClockHeader), +} + +pub struct ControlHeaderBlock { + pub headers: Vec, +} +``` + +The component does not define or parse these headers as part of its +instruction set. A header can configure multiple devices, machine-wide +scheduling, source symbols, or the program's module metadata. + +## Namespaces and parsing + +Components define local instruction grammar. The composite defines the public +namespace: + +```text +processor::step 100 +processor::reset +waveform::play 50 +``` + +The generated composite parser delegates the namespaced instruction to the +component parser: + +```text +processor::step 100 + -> SurfaceInstruction::Processor( + processor::syntax::Instruction::Step(...) + ) +``` + +The same component syntax can be mounted more than once: + +```text +cpu_a::step 100 +cpu_b::step 100 +``` + +The component does not need to know which alias or device field selected it. + +## ModuleSyntax + +`ModuleSyntax` describes one complete source dialect: + +```rust +pub trait ModuleSyntax { + type Instruction: SurfaceInstruction; + type Value; + type Type; + type Header: SstHeader; +} +``` + +For a composite, the associated types have these owners: + +```text +ModuleSyntax::Instruction + generated sum of component surface instructions + +ModuleSyntax::Value + generated sum of component and core source values + +ModuleSyntax::Type + generated sum of component and core source types + +ModuleSyntax::Header + composite-owned parsed section-header syntax +``` + +The composite generates a marker and implementation: + +```rust +pub mod control_machine { + pub mod syntax { + pub struct Module; + + pub enum Instruction { + Processor(processor::syntax::Instruction), + Waveform(waveform::syntax::Instruction), + } + + pub enum Type { + Processor(processor::syntax::Type), + Waveform(waveform::syntax::Type), + } + + pub enum Value { + Processor(processor::syntax::Value), + Waveform(waveform::syntax::Value), + } + + impl ::vihaco::ModuleSyntax for Module { + type Instruction = Instruction; + type Value = Value; + type Type = Type; + type Header = ControlHeaderBlock; + } + } +} +``` + +`ParsedModule` becomes: + +```rust +pub struct ParsedModule +where + S: ModuleSyntax, +{ + pub header: S::Header, + pub functions: Vec>, +} +``` + +The parser and resolver now operate on: + +```rust +ParsedModule +``` + +rather than independent instruction, type, and header parameters. + +## Semantic analysis and lowering + +Parsing produces syntax values and types. The composite performs semantic +analysis during resolution, using the marked `#[program]` field to resolve +labels, types, constants, and other module-wide symbols before constructing +runtime instruction products. + +For example: + +```text +processor::br @loop + -> processor::syntax::Instruction::Branch(Value::Label(...)) + -> program.resolve_label(...) + -> processor::instruction::Branch { target: u32 } +``` + +This keeps machine-specific policy in the composite: + +- route identity; +- one-to-many expansion; +- source sugar; +- device selection; +- scheduling or timing policy; +- conversions requiring composite state. + +The component defines what its instruction means locally. The composite +decides which machine route receives it. + +An illustrative generated resolver implementation is: + +```rust +impl ControlMachineSyntaxResolver for ControlMachine { + fn lower_processor( + &mut self, + instruction: processor::syntax::Instruction, + ) -> Result, ControlFault> { + match instruction { + processor::syntax::Instruction::Step(value) => { + let value = match value { + processor::syntax::Value::U32(value) => value, + other => { + return Err(ControlFault::type_error( + "processor::step expects a u32 value", + other, + )); + } + }; + + Ok(vec![ControlRuntimeInstruction::ProcessorStep( + processor::instruction::Step { value }, + )]) + } + processor::syntax::Instruction::Branch(value) => { + let label = match value { + processor::syntax::Value::Label(label) => label, + other => { + return Err(ControlFault::type_error( + "processor::br expects a label", + other, + )); + } + }; + let target = self.program.resolve_label(&label)?; + + Ok(vec![ControlRuntimeInstruction::ProcessorBranch( + processor::instruction::Branch { target }, + )]) + } + processor::syntax::Instruction::Add(ty) => { + let ty = self.program.resolve_type(ty)?; + + Ok(vec![ControlRuntimeInstruction::ProcessorAdd( + processor::instruction::Add { ty }, + )]) + } + processor::syntax::Instruction::Reset => Ok(vec![ + ControlRuntimeInstruction::ProcessorReset( + processor::instruction::Reset, + ), + ]), + } + } +} +``` + +Broad value operands may produce semantic errors during resolution. Syntax +declarations may instead constrain an operand to a particular value variant +when earlier parser rejection is preferable: + +```rust +instruction { + Step(value: U32) = "'step $value"; + Branch(target: Label) = "'br $target"; +} +``` + +## Header resolution + +Parsing a header and resolving a header are separate operations. The generic +resolver consumes the complete parsed module: + +```rust +pub trait Resolve +where + S: ModuleSyntax, +{ + type Module; + + fn resolve_module( + &mut self, + parsed: ParsedModule, + ) -> eyre::Result; +} +``` + +Generated composites also expose a header-resolution boundary: + +```rust +pub trait ControlMachineSyntaxResolver { + fn resolve_header( + &mut self, + header: ControlHeaderBlock, + ) -> Result<(), ControlMachineFault>; + + // Named instruction lowerers follow. +} +``` + +The header resolver may mutate composite state, produce program module +metadata, configure multiple devices, or validate machine-wide constraints. +It must run before module installation and must not depend on arbitrary live +child-device state during source resolution. + +If resolved headers populate the program module's `Info`, +`BuildProgramModule` needs an explicit metadata assignment operation such as: + +```rust +fn set_info(module: &mut Self::Module, info: Self::Info); +``` + +The exact metadata flow should follow the Acamar pattern: + +```text +ControlHeaderBlock + -> composite header resolver + -> resolved Info + -> runtime module installation +``` + +## Loading and nesting + +Generated loading uses the composite's complete syntax dialect: + +```rust +let parsed = ParsedModule:: + ::parse_section(section.clone())?; + +let module = self.resolve_parsed(parsed)?; +``` + +The loading sequence is: + +```text +parse section + -> ParsedModule + -> resolve composite-owned header + -> lower component surface instructions + -> resolve module metadata + -> build runtime module + -> install runtime module +``` + +Each nested composite owns its own generated module syntax: + +```text +RootMachine::syntax::Module +ChildMachine::syntax::Module +``` + +Recursive loading requires only: + +```text +ChildMachine: LoadSstSubtree +``` + +The parent does not name the child's source types, instruction enum, or header +type. + +## Implementation order + +1. Add component instruction-set syntax products for instructions, values, and + types; keep them optional for runtime-only components. +2. Add composite-generated instruction, value, and type sums. +3. Add composite-owned header syntax and header-resolution hooks. +4. Add `ModuleSyntax` and refactor `ParsedModule`/`ParsedFunction`. +5. Update `Resolve` and migrate standalone syntax tests. +6. Update generated composite parsing, semantic analysis, lowering, and module + loading. +7. Add program-backed resolution for labels, types, constants, and metadata. +8. Decide and implement the module metadata assignment operation, if needed. +9. Rename `LoadOwnSstSection`/`LoadSstSection` to the program/subtree model. +10. Generate recursive nested composite loading. +11. Add Acamar-shaped header, source-sum, semantic-analysis, and nested-loading + tests. +12. Migrate the demo and documentation. + +## Non-goals + +Components do not own SST section headers, device aliases, route identity, or +machine-wide lowering policy. Components may own parsers for their local +instruction and value/type syntax, but they do not become aware of the +composite that mounts them. diff --git a/vision/module-syntax-plan.md b/vision/module-syntax-plan.md new file mode 100644 index 00000000..e3f61720 --- /dev/null +++ b/vision/module-syntax-plan.md @@ -0,0 +1,414 @@ +# Module Syntax Rewrite: Parallel Implementation Plan + +## Purpose and boundary + +This is the implementation plan for the module-syntax portion of the larger +[composite syntax/runtime plan](composite-syntax-runtime-plan.md). It turns the +current design into small, parallelizable work packages and identifies the +integration points that must be landed in order. + +The target ownership boundary is: + +```text +component + local instruction, value, and source-type syntax + local parser implementations + runtime instruction products + +composite + complete module dialect + public namespaces and aliases + source-syntax sums + SST section headers + semantic analysis and lowering policy + runtime route selection +``` + +The source dialect consumed by one SST module is represented by one +`ModuleSyntax` type. Its instruction, value, and type syntax are composed from +the mounted components; its header syntax is owned by the composite that owns +the SST section. + +This plan does not redesign runtime effect dispatch. That work is already +largely present in the repository and remains covered by the companion plan. + +## Current repository state + +The following pieces are already present or substantially implemented. Agents +should preserve them and build on their current APIs rather than recreate them: + +- `vihaco-runtime-derive` generates composite `syntax` and `runtime` modules, + surface parsers, route dispatch, named message resolvers, and program-module + loading scaffolding. +- `InstallProgramModule` and `BuildProgramModule` exist in + `vihaco-module`; `ProgramImage` provides the standard implementation. +- Generated loading validates and forwards direct child sections. The recent + generated loader change makes child forwarding separate from loading the + composite's own section. +- Parser patterns no longer require an instruction `head` and namespaced + instruction tokens are supported. +- Runtime route tests, basic installation tests, message resolution, string + interning, and child forwarding exist. + +The main unfinished seams are: + +- `vihaco-syntax` still defines `ParsedModule`, + `ParsedFunction`, and `Resolve`. +- There is no shared `ModuleSyntax` trait or component `InstructionSet` syntax + contract in the public API. +- Generated component source sums and composite-owned header syntax are not + yet wired through the parser and resolver. +- Generated loading still names `LoadOwnSstSection` and `LoadSstSection` and + still carries independent surface-type/header parameters. +- The existing demo and parser documentation still describe the old generic + syntax API. + +## Agent execution rules + +Each agent works on one work package and must: + +1. inspect the current code and tests before editing; +2. keep changes inside the listed ownership boundary where possible; +3. add or update focused tests with the implementation; +4. run the narrowest relevant tests, formatting, and compilation checks; +5. report changed files, public API decisions, and unresolved integration + assumptions. + +Do not have parallel agents edit the same implementation file. If an API + decision affects multiple work packages, the contract agent lands the + contract first and dependent agents rebase or apply the contract before + implementation. Generated files under `.agents/` and `.claude/` are not + hand-edited. + +## Dependency graph + +```text + ┌─ B parser-model migration ─┐ +A contracts ─────────────┼─ C component syntax API ────┼─ E composite codegen + └─ D loader trait rename ─────┘ │ + ├─ F lowering/loading integration + └─ G nested loading + +F ── H metadata/header semantics ── I integration tests ── J demo/docs +C ────────────────────────────────┘ +``` + +The independent tracks can start together after the repository audit, but the +integration agents must consume the contracts from A. Work packages that touch +the same macro call sites are intentionally sequenced to keep merge conflicts +small. + +## Step 0 — Coordinator audit and contract freeze + +**Owner:** coordinator, before parallel implementation begins. + +Record the current behavior and establish the exact public names to use. Read +the current `vihaco-syntax`, `vihaco-module`, `vihaco-runtime-derive` composite +codegen/loadable modules, generated SST tests, demo, and parser guide. + +Freeze these decisions in the implementation PR description or a short design +note before agents proceed: + +- `ModuleSyntax` has associated `Instruction`, `Value`, `Type`, and `Header` + types; `Header` implements `SstHeader`. +- `ParsedModule` contains `S::Header` and `Vec>`. +- `ParsedFunction` obtains parameter/return types and body instructions from + `S`. +- `Resolve` consumes the complete parsed module, including its header. +- component syntax is optional; runtime-only components remain valid; +- headers are composite-owned and are resolved before program installation; +- the new loading names are `LoadSstProgram` and `LoadSstSubtree`; +- resolved header metadata is assigned through an explicit builder operation + if it belongs in the installed module `Info`. + +**Gate:** `cargo check --workspace` on the baseline and a written list of +files each parallel agent owns. + +## Step 1 — Shared syntax contracts and data model + +**Agent A — `vihaco-syntax` contract owner** + +Implement only the foundational public model and its unit tests: + +- add and export `ModuleSyntax`; +- change `ParsedModule` to `ParsedModule`; +- change `ParsedFunction` and `Param` to use the module dialect; +- update the `Parse` implementations and `parse_section` to derive all + syntax types from `S`; +- update `Resolve` to accept `ParsedModule`; +- preserve parsed headers as source syntax, distinct from runtime metadata; +- migrate the existing `vihaco-syntax` tests to a small test dialect marker. + +Do not implement composite macro generation in this step. Keep the migration +mechanically useful for standalone consumers. + +**Deliverable:** `vihaco-syntax` compiles with no old generic model in its +public API; focused syntax tests pass. + +## Step 2 — Component instruction-set syntax contract + +**Agent B — component/parser API owner** + +Define the optional component-side syntax product and test it independently of +composites: + +- add `InstructionSet` with surface `Instruction`, `Value`, and `Type` + associated types; +- establish the required bounds for instruction parsing and the + `SurfaceInstruction` marker; +- expose the contract through the appropriate facade crates; +- add a representative component syntax declaration or test fixture using the + existing pattern parser; +- verify a component can provide local instruction/value/type parsers without + knowing its mounted alias, device code, composite, or runtime route; +- verify a runtime-only component needs no syntax implementation. + +If the declarative component `syntax {}` block is not yet implemented, do not +expand the macro grammar in this work package. Document the exact input shape +that Agent E will consume and leave macro parsing/codegen to that agent. + +**Deliverable:** a stable component syntax contract and parser-level tests. + +## Step 3 — Loader capability rename and compatibility migration + +**Agent C — `vihaco-module` loader owner** + +Rename the loading capabilities to match the program/subtree model: + +- `LoadOwnSstSection` → `LoadSstProgram`; +- `LoadSstSection` → `LoadSstSubtree`; +- update docs, facade re-exports, trait bounds, and existing tests; +- keep the semantic distinction explicit: own-program loading first, then + recursive child forwarding; +- decide whether a temporary deprecated alias is needed for a staged migration; + if not, update all in-repository consumers in this step. + +Do not change generated composite behavior beyond the trait names. Do not add +the new module-dialect bounds here; that belongs to the loading integration +step. + +**Deliverable:** loader traits have the final names and the existing child +forwarding/install tests still pass. + +## Step 4 — Composite source-sum generation + +**Agent D — `vihaco-runtime-derive/src/composite` codegen owner** + +Using the contracts from Steps 1–2, generate the complete composite syntax +product: + +- a namespaced `syntax::Module` implementing `ModuleSyntax`; +- `syntax::Instruction`, `syntax::Value`, and `syntax::Type` sum enums for + participating `#[syntax]`/device contributions; +- parser implementations that delegate after resolving the composite-owned + public namespace or alias; +- support for mounting one component syntax more than once under different + aliases; +- preserve explicit enum wrapping so duplicate local spellings remain + diagnosable rather than silently merged; +- retain composite-only syntax hooks where the current macro already supports + them. + +The generated runtime instruction enum remains separate from the surface +instruction enum. Do not make runtime products implement source parsing by +default. + +Add compile-pass coverage for one component, two components, aliases, and a +runtime-only device. Add compile-fail coverage for missing syntax metadata and +ambiguous/invalid namespace declarations if those diagnostics are part of the +chosen API. + +**Deliverable:** a composite can expose a complete generated source dialect, +but loading need not use it yet. + +## Step 5 — Composite-owned header syntax and resolution contract + +**Agent E — header/metadata owner** + +Implement the composite-side header boundary, keeping it separate from +component instruction sets: + +- add the syntax declaration/input needed for a composite-owned header block; +- generate the header type/parse hook and the public syntax-resolver method; +- ensure a header can configure multiple devices or machine-wide metadata; +- resolve the header before lowering/installing instructions; +- make failures use the composite error at the resolver boundary and gain + section/function/instruction context at the outer `eyre::Result` boundary; +- add `BuildProgramModule::set_info` (or the selected equivalent) only if the + chosen metadata flow requires it, with a standard `ProgramImage` behavior; +- test that resolved header metadata survives installation and that invalid + headers do not partially install a program. + +Header parsing must produce source syntax. It must not directly expose or +mutate arbitrary live child-device state during module parsing. + +**Deliverable:** a composite-owned header can be parsed, resolved, and carried + into installed module metadata when required. + +## Step 6 — Generated resolver and SST loading integration + +**Agent F — integration owner; starts after Steps 1, 3, and 4** + +Update generated loading to use the complete dialect and resolver pipeline: + +- parse `ParsedModule` with no caller-supplied + instruction/type/header generic parameters; +- make `load_parsed` resolve the header rather than discarding it; +- lower each surface instruction into `Vec` so one-to-one + and one-to-many expansion are both supported; +- assign final runtime instruction addresses before resolving labels/source + symbols that refer to expanded code; +- build a temporary module and install it only after parsing, header + resolution, lowering, and validation succeed; +- use the renamed `LoadSstProgram`/`LoadSstSubtree` traits; +- retain source function and instruction context when a named lowerer fails; +- avoid requiring capabilities (strings, constants, bytecode, or metadata) + that a particular program container does not use. + +The generated resolver trait should contain the header resolver and named +lowerers. Direct instruction mappings remain generated dispatch and do not +create unnecessary user methods. + +**Deliverable:** `load_source` and `load_parsed` use one composite module +dialect end-to-end and preserve one-shot installation semantics. + +## Step 7 — Nested composite subtree loading + +**Agent G — recursive loading owner; starts after Steps 3 and 6** + +Complete recursive loading without leaking child syntax types into the parent: + +- generated composites load their own `#[program]` section through + `LoadSstProgram`; +- they then forward each direct child through `LoadSstSubtree`; +- leaf devices implement `LoadSstSubtree` directly; +- each nested composite parses with its own generated + `Child::syntax::Module`; +- parents require only the child subtree capability and do not name child + parser enums or headers; +- validate expected child names and reject duplicates/missing sections with + useful section context. + +Add a nested fixture where parent and child deliberately use different +instruction, type, and header syntax. Confirm child loading happens only after +the parent has accepted its own program section and that a failure leaves the +parent program uninstalled. + +**Deliverable:** recursive SST loading works with independent nested dialects. + +## Step 8 — Semantic analysis and program-backed resolution + +**Agent H — resolver semantics owner; starts after Step 6** + +Add the semantic cases needed by the new source sums: + +- resolve labels against final runtime addresses; +- resolve source types and report type mismatches clearly; +- resolve constants, strings, and source symbols through the program/context + capabilities actually required by the composite; +- support composite policy such as source sugar and one-to-many expansion; +- keep live machine-state decisions in runtime message resolution, not source + resolution; +- add diagnostics containing function and instruction location where the + lowerer reports an error. + +Use broad value operands where semantic errors are preferable; use narrower +value/type syntax only where parser rejection is intentionally part of the +language contract. + +**Deliverable:** representative labels, types, constants, source symbols, and +one-to-many lowering are tested through `load_parsed`. + +## Step 9 — Integration and regression coverage + +**Agent I — test owner; starts after Steps 6–8** + +Add coverage across crate boundaries: + +- component instruction-set parsing; +- generated instruction/value/type source sums and aliases; +- `ModuleSyntax` parsing and `Resolve`; +- header resolution, invalid-header diagnostics, and metadata installation; +- custom program containers implementing the minimum builder/install traits; +- one-to-many lowering and final label addresses; +- source-location/function/instruction error context; +- independent nested composite dialects and subtree loading; +- runtime-only components mounted beside syntax-bearing components. + +Update trybuild `.stderr` fixtures only when diagnostics intentionally change. +Run workspace tests, doctests, clippy, and format as applicable. License checks +remain part of the final coordinator gate. + +## Step 10 — Demo, guide, and cross-plan cleanup + +**Agent J — migration/docs owner; starts after Step 9** + +Migrate the demo and documentation to the final API: + +- give demo components optional local instruction/value/type syntax; +- load the composite program through its generated module dialect; +- resolve a composite-owned header before installation; +- forward a nested debug/observer section through `LoadSstSubtree`; +- update parser and composite guides from `ParsedModule` and + `Resolve` to `ParsedModule` and `Resolve`; +- update examples and any companion vision docs that still describe the old + loader names or independent syntax parameters. + +**Deliverable:** demo, docs, and doctests describe the same API that the tests +exercise. + +## Coordinator integration gates + +After each convergence point, the coordinator owns conflict resolution and +API consistency: + +### Gate 1 — after Steps 1–3 + +Run `cargo fmt --all -- --check`, `cargo check --workspace`, and focused syntax, +loader, and existing macro tests. Confirm all public re-exports use one set of +names and no old generic API remains accidentally exposed. + +### Gate 2 — after Steps 4–6 + +Run parser derive tests, runtime macro compile tests, generated SST loading +tests, and `cargo test --workspace --all-targets`. Inspect generated code for +the no-partial-install guarantee and for correct error conversion. + +### Gate 3 — after Steps 7–8 + +Run nested loading, metadata, label, and semantic diagnostics tests. Confirm +that parent composites do not depend on child source types and that final +addresses are based on lowered runtime instructions. + +### Final gate + +Run the repository checklist: + +```text +cargo fmt --all -- --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace --all-targets +cargo test --workspace --doc +hawkeye check +``` + +## Compatibility and migration policy + +This is a deliberate public API migration. The preferred end state removes the +old `ParsedModule`, `Resolve`, `LoadOwnSstSection`, and +`LoadSstSection` names. If an intermediate commit needs compatibility aliases, +they must be clearly deprecated and removed before the final integration gate; +the generated macro API and documentation must use only the new names. + +## Non-goals + +This plan does not add: + +- bytecode loading or encoding; +- mandatory parsers for runtime-only components; +- a universal lookup API for strings or constants; +- generated effect-handler traits; +- runtime route selection from arbitrary live device state during source + resolution; +- compatibility with old `head`-based parser declarations; +- generated scheduling, resume, or continuation policy. diff --git a/vision/sst-resolution.md b/vision/sst-resolution.md index e85e514d..9c4d5567 100644 --- a/vision/sst-resolution.md +++ b/vision/sst-resolution.md @@ -83,7 +83,7 @@ shapes rather than aliases for one catch-all `String`. A parsed function's parameter and return types likewise use an author-selected surface type: ```text -ParsedModule +ParsedModule ``` The complete ownership and runtime relationship is defined in @@ -133,7 +133,7 @@ creating an empty executable instruction sum. Pattern parsing and module resolution are consecutive but distinct boundaries. Parsing always constructs a surface instruction and author-defined module type products. -`Resolve` then uses module-wide context to construct +`Resolve` then uses module-wide context to construct runtime instructions, constants, and runtime type metadata: - Labels and symbolic branch targets require symbol resolution. @@ -151,8 +151,8 @@ pattern parsing: source text -> surface instruction module resolution: - ParsedModule - -> Resolve + ParsedModule + -> Resolve -> Module runtime message resolution: @@ -176,6 +176,6 @@ A consistent naming direction is: - `SurfaceInstruction` for the types constructed by the pattern parser. - `Instruction` for an individual runtime operation. - `MachineInstruction` or `InstructionSet` for the generated runtime sum. -- `Resolve` for module lowering. +- `Resolve` for module lowering. The exact identifiers remain an API decision; the three roles must remain visible. diff --git a/vision/types-and-values.md b/vision/types-and-values.md index c75a6595..433a735e 100644 --- a/vision/types-and-values.md +++ b/vision/types-and-values.md @@ -141,7 +141,7 @@ containment: | Closed value carrier for a particular architecture | The data-model or machine author | | Surface grammar for values and types | The surface product that implements `Parse` | | Module-level surface type | The author-selected SST dialect | -| Source type checking and lowering | `Resolve` | +| Source type checking and lowering | `Resolve` | | Storage and invariant-preserving mutation | The component | | Concrete types used by fields and routes | The composite declaration | | Cross-domain conversion semantics | An explicit author-selected instruction, adapter, or handler | @@ -220,22 +220,22 @@ instructions fail at the parser boundary rather than becoming generic mnemonic/o Module-level signatures must use an author-selected surface type: ```rust -pub struct ParsedModule +pub struct ParsedModule where - I: SurfaceInstruction, + S: ModuleSyntax, { - pub header: H, - pub functions: Vec>, + pub header: S::Header, + pub functions: Vec>, } -pub struct ParsedFunction +pub struct ParsedFunction where - I: SurfaceInstruction, + S: ModuleSyntax, { pub name: String, - pub params: Vec>, - pub return_ty: Option, - pub body: Vec, + pub params: Vec>, + pub return_ty: Option, + pub body: Vec, } pub struct Param { @@ -263,8 +263,8 @@ Types and values follow the same stage boundary as instructions: ```text SST text -> pattern parser - -> ParsedModule - -> Resolve + -> ParsedModule + -> Resolve -> Module -> runtime program image ``` From 7c87984057f7606bdb4b75b44eb45239c9bff3b2 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 11 Aug 2026 11:42:25 -0400 Subject: [PATCH 12/15] Added follow up observation for observers that emit effects --- .../src/composite/codegen.rs | 89 +++- .../src/composite/syntax.rs | 38 +- .../src/composite/validate.rs | 47 +- crates/vihaco-runtime/src/observe.rs | 2 +- .../vihaco-runtime/tests/runtime_contract.rs | 2 +- crates/vihaco-stdlib/src/observer/stdio.rs | 6 +- .../tests/runtime_macro_crate_override.rs | 79 ++- demos/examples/demo/stdlib/debug_trace.rs | 4 +- docs/examples/observe.rs | 6 +- vision/typed-observation-trees.md | 458 ++++++++++++++++++ 10 files changed, 681 insertions(+), 50 deletions(-) create mode 100644 vision/typed-observation-trees.md diff --git a/crates/vihaco-runtime-derive/src/composite/codegen.rs b/crates/vihaco-runtime-derive/src/composite/codegen.rs index aab10e43..1e6fe486 100644 --- a/crates/vihaco-runtime-derive/src/composite/codegen.rs +++ b/crates/vihaco-runtime-derive/src/composite/codegen.rs @@ -7,8 +7,8 @@ use quote::{format_ident, quote, quote_spanned}; use syn::{Field, Generics, Ident, Result, Type}; use super::syntax::{ - CompositeDeclaration, Handler, HeaderDeclaration, MessageSource, RouteDeclaration, - SyntaxDeclaration, SyntaxMapping, + CompositeDeclaration, Handler, HeaderDeclaration, MessageSource, ObserverDeclaration, + RouteDeclaration, SyntaxDeclaration, SyntaxMapping, }; use crate::common::{resolve_root, retain_generics}; @@ -43,6 +43,60 @@ fn strip_consumed_field_attrs(mut field: Field) -> Field { field } +fn generate_observers( + root: &TokenStream2, + observers: &[ObserverDeclaration], + input: &TokenStream2, + marker: &TokenStream2, + error: &Type, + fields: &[super::validate::FieldMetadata], +) -> TokenStream2 { + let field_ty = |field: &Ident| -> &Type { + &fields + .iter() + .find(|candidate| candidate.ident == *field) + .expect("validated observer field") + .ty + }; + let branches = observers.iter().map(|observer| { + let field = &observer.field; + let observer_ty = field_ty(field); + let output = quote!(<#observer_ty as #root::Observe<#input, #marker>>::Effect); + let nested = generate_observers(root, &observer.observers, &output, marker, error, fields); + let terminal = if observer.observers.is_empty() && observer.handler.is_none() { + quote_spanned! {observer.field.span()=> + let _: #root::NoEffect = effect; + } + } else { + quote! {} + }; + let handler = observer.handler.as_ref().map(|handler| match handler { + Handler::With(method) => quote! { + self.#method(effect) + .map_err(::std::convert::Into::<#error>::into)?; + }, + Handler::Absorb(field) => { + let destination_ty = field_ty(field); + quote! { + <#destination_ty as #root::Absorb<#output>>::absorb(&mut self.#field, effect) + .map_err(::std::convert::Into::<#error>::into)?; + } + } + }).unwrap_or_default(); + quote! { + for effect in <#observer_ty as #root::Observe<#input, #marker>>::observe( + &mut self.#field, + &effect, + ).map_err(::std::convert::Into::<#error>::into)? { + #nested + #handler + #terminal + } + } + }); + quote!( #( #branches )* ) +} + fn syntax_generics(generics: &Generics, syntax: &[SyntaxDeclaration]) -> Generics { let payloads = syntax .iter() @@ -905,20 +959,20 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result::into)? }, }; - let observers = route.observers.iter().map(|observer| { - let observer_ty = field_ty(observer); - quote! { - <#observer_ty as #root::Observe< - <#target_ty as #root::Execute<#payload>>::Effect, - #route_module::#marker - >>::observe(&mut self.#observer, &effect) - .map_err(::std::convert::Into::<#error_type>::into)?; - } - }); + let component_effect = quote!(<#target_ty as #root::Execute<#payload>>::Effect); + let route_marker = quote!(#route_module::#marker); + let observers = generate_observers( + &root, + &route.observers, + &component_effect, + &route_marker, + error_type, + &fields_metadata, + ); let effect_handling = if route.handler.is_some() { quote! { for effect in result.effects { - #( #observers )* + #observers >::Effect, #route_module::#marker @@ -927,11 +981,16 @@ pub(super) fn try_expand(declaration: CompositeDeclaration) -> Result - let _: #root::NoEffect = effect; + let no_effect_assertion = if route.observers.is_empty() { + quote_spanned! {route.variant.span()=> + let _: #root::NoEffect = effect; + } + } else { + quote! {} }; quote! { for effect in result.effects { + #observers #no_effect_assertion } } diff --git a/crates/vihaco-runtime-derive/src/composite/syntax.rs b/crates/vihaco-runtime-derive/src/composite/syntax.rs index 81f39ef8..695dd6d0 100644 --- a/crates/vihaco-runtime-derive/src/composite/syntax.rs +++ b/crates/vihaco-runtime-derive/src/composite/syntax.rs @@ -57,7 +57,13 @@ pub(super) struct RouteDeclaration { pub(super) payload: Type, pub(super) target: Ident, pub(super) message: MessageSource, - pub(super) observers: Vec, + pub(super) observers: Vec, + pub(super) handler: Option, +} + +pub(super) struct ObserverDeclaration { + pub(super) field: Ident, + pub(super) observers: Vec, pub(super) handler: Option, } @@ -317,15 +323,32 @@ impl Parse for RouteDeclaration { fn parse_effects( input: ParseStream<'_>, - observers: &mut Vec, + observers: &mut Vec, handler: &mut Option, ) -> Result<()> { while !input.is_empty() { if input.peek(observe) { input.parse::()?; let mut names = Vec::new(); + let mut had_block = false; loop { - names.push(input.parse::()?); + let field = input.parse::()?; + let (nested, nested_handler) = if input.peek(syn::token::Brace) { + had_block = true; + let body; + syn::braced!(body in input); + let mut nested = Vec::new(); + let mut nested_handler = None; + parse_effects(&body, &mut nested, &mut nested_handler)?; + (nested, nested_handler) + } else { + (Vec::new(), None) + }; + names.push(ObserverDeclaration { + field, + observers: nested, + handler: nested_handler, + }); if input.peek(Token![,]) { input.parse::()?; } else { @@ -336,7 +359,11 @@ fn parse_effects( return Err(input.error("`observe` requires at least one field")); } observers.extend(names); - input.parse::()?; + if input.peek(Token![;]) { + input.parse::()?; + } else if !had_block && !input.is_empty() { + return Err(input.error("expected `;` after observer declaration")); + } } else if input.peek(absorb) || input.peek(handle) { let is_absorb = input.peek(absorb); if is_absorb { @@ -365,9 +392,6 @@ fn parse_effects( } } - if handler.is_none() { - return Err(input.error("effects block is missing an effect handler")); - } Ok(()) } diff --git a/crates/vihaco-runtime-derive/src/composite/validate.rs b/crates/vihaco-runtime-derive/src/composite/validate.rs index 56d4b9a7..f8b4f0da 100644 --- a/crates/vihaco-runtime-derive/src/composite/validate.rs +++ b/crates/vihaco-runtime-derive/src/composite/validate.rs @@ -223,21 +223,7 @@ pub(super) fn validate_routes(routes: &[RouteDeclaration], fields: &[FieldMetada } _ => {} } - let mut observer_names = BTreeSet::new(); - for observer in &route.observers { - if !field_names.contains(&observer.to_string()) { - return Err(syn::Error::new( - observer.span(), - format!("unknown observer field `{observer}`"), - )); - } - if !observer_names.insert(observer.to_string()) { - return Err(syn::Error::new( - observer.span(), - format!("duplicate observer field `{observer}`"), - )); - } - } + validate_observers(&route.observers, &field_names)?; if let Some(Handler::Absorb(field)) = &route.handler && !field_names.contains(&field.to_string()) { @@ -250,6 +236,37 @@ pub(super) fn validate_routes(routes: &[RouteDeclaration], fields: &[FieldMetada Ok(()) } +fn validate_observers( + observers: &[super::syntax::ObserverDeclaration], + field_names: &BTreeSet, +) -> Result<()> { + let mut names = BTreeSet::new(); + for observer in observers { + if !field_names.contains(&observer.field.to_string()) { + return Err(syn::Error::new( + observer.field.span(), + format!("unknown observer field `{}`", observer.field), + )); + } + if !names.insert(observer.field.to_string()) { + return Err(syn::Error::new( + observer.field.span(), + format!("duplicate observer field `{}`", observer.field), + )); + } + validate_observers(&observer.observers, field_names)?; + if let Some(super::syntax::Handler::Absorb(field)) = &observer.handler + && !field_names.contains(&field.to_string()) + { + return Err(syn::Error::new( + field.span(), + format!("unknown effect destination field `{field}`"), + )); + } + } + Ok(()) +} + pub(super) fn validate_syntax( syntax: &[SyntaxDeclaration], routes: &[RouteDeclaration], diff --git a/crates/vihaco-runtime/src/observe.rs b/crates/vihaco-runtime/src/observe.rs index 178170fd..2aa370ca 100644 --- a/crates/vihaco-runtime/src/observe.rs +++ b/crates/vihaco-runtime/src/observe.rs @@ -5,7 +5,7 @@ use crate::Effects; /// A non-consuming effect observer selected by a composite-specific route marker. /// -/// Composite route generation currently discards follow-up effects. +/// The associated effect is routed to nested observers by generated composites. pub trait Observe { type Effect; type Error; diff --git a/crates/vihaco-runtime/tests/runtime_contract.rs b/crates/vihaco-runtime/tests/runtime_contract.rs index dabf122a..04da4b35 100644 --- a/crates/vihaco-runtime/tests/runtime_contract.rs +++ b/crates/vihaco-runtime/tests/runtime_contract.rs @@ -43,7 +43,7 @@ impl Absorb for Component { } impl Observe for Component { - type Effect = (); + type Effect = NoEffect; type Error = Fault; fn observe(&mut self, effect: &Effect) -> Result, Self::Error> { diff --git a/crates/vihaco-stdlib/src/observer/stdio.rs b/crates/vihaco-stdlib/src/observer/stdio.rs index 7c084082..b5026f5b 100644 --- a/crates/vihaco-stdlib/src/observer/stdio.rs +++ b/crates/vihaco-stdlib/src/observer/stdio.rs @@ -4,7 +4,7 @@ use std::io::Write; use eyre::Result; -use vihaco_runtime::{Effects, Observe}; +use vihaco_runtime::{Effects, NoEffect, Observe}; #[derive(Debug, Clone)] pub struct StdoutEffect(pub String); @@ -26,10 +26,10 @@ impl StdoutObserver { } impl Observe for StdoutObserver { - type Effect = (); + type Effect = NoEffect; type Error = eyre::Report; - fn observe(&mut self, effect: &StdoutEffect) -> Result> { + fn observe(&mut self, effect: &StdoutEffect) -> Result> { self.write_stdout(&effect.0)?; Ok(Effects::none()) } diff --git a/crates/vihaco/tests/runtime_macro_crate_override.rs b/crates/vihaco/tests/runtime_macro_crate_override.rs index 4e976e82..b88487b6 100644 --- a/crates/vihaco/tests/runtime_macro_crate_override.rs +++ b/crates/vihaco/tests/runtime_macro_crate_override.rs @@ -4,7 +4,8 @@ use chumsky::Parser as _; use eyre::Result; use vihaco::{ - Effects, Execute, Execution, Observe, SstFile, SstGlobalContext, StepResult, VERSION, composite, + Effects, Execute, Execution, NoEffect, Observe, SstFile, SstGlobalContext, StepResult, VERSION, + composite, }; use vihaco_parser::{Ident, Parse}; @@ -17,6 +18,7 @@ pub struct TestInstruction; pub struct TestMessage(u32); struct TestEffect; +struct IntermediateEffect; struct TestComponent { received_message: Option, } @@ -65,10 +67,40 @@ struct TestObserver { } impl Observe for TestObserver { - type Effect = (); + type Effect = NoEffect; type Error = eyre::Report; - fn observe(&mut self, _effect: &TestEffect) -> Result> { + fn observe(&mut self, _effect: &TestEffect) -> Result> { + self.observed = true; + Ok(Effects::none()) + } +} + +#[derive(Default)] +struct TransformObserver { + observed: bool, +} + +#[derive(Default)] +struct SinkObserver { + observed: bool, +} + +impl Observe for TransformObserver { + type Effect = IntermediateEffect; + type Error = eyre::Report; + + fn observe(&mut self, _effect: &TestEffect) -> Result> { + self.observed = true; + Ok(Effects::one(IntermediateEffect)) + } +} + +impl Observe for SinkObserver { + type Effect = NoEffect; + type Error = eyre::Report; + + fn observe(&mut self, _effect: &IntermediateEffect) -> Result> { self.observed = true; Ok(Effects::none()) } @@ -82,6 +114,8 @@ composite! { #[device(0x01)] component: TestComponent, observer: TestObserver, + transform: TransformObserver, + sink: SinkObserver, #[program] program: vihaco::ProgramImage, } @@ -101,6 +135,15 @@ composite! { handle with handle_effect; } } + + Nested(TestInstruction) => component { + message with resolve_nested_message; + effects { + observe transform { + observe sink; + } + } + } } } @@ -108,6 +151,10 @@ impl test_machine::runtime::MessageResolver for TestMachine { fn resolve_message(&mut self, _instruction: &TestInstruction) -> Result { Ok(TestMessage(42)) } + + fn resolve_nested_message(&mut self, _instruction: &TestInstruction) -> Result { + Ok(TestMessage(42)) + } } impl TestMachine { @@ -138,6 +185,8 @@ fn runtime_macros_honor_explicit_crate_override() { received_message: None, }, observer: TestObserver::default(), + transform: TransformObserver::default(), + sink: SinkObserver::default(), program: vihaco::ProgramImage::new(), }; let outcome = machine @@ -152,6 +201,26 @@ fn runtime_macros_honor_explicit_crate_override() { assert_eq!(metadata.devices[0].name, "component"); } +#[test] +fn nested_observers_receive_concrete_follow_up_effects() { + let mut machine = TestMachine { + component: TestComponent { + received_message: None, + }, + observer: TestObserver::default(), + transform: TransformObserver::default(), + sink: SinkObserver::default(), + program: vihaco::ProgramImage::new(), + }; + + machine + .execute_generated(&TestMachineInstruction::Nested(TestInstruction)) + .unwrap(); + + assert!(machine.transform.observed); + assert!(machine.sink.observed); +} + #[test] fn generated_program_loader_builds_and_installs_module() { let parsed = vihaco::syntax::ParsedModule { @@ -172,6 +241,8 @@ fn generated_program_loader_builds_and_installs_module() { received_message: None, }, observer: TestObserver::default(), + transform: TransformObserver::default(), + sink: SinkObserver::default(), program: vihaco::ProgramImage::new(), }; @@ -196,6 +267,8 @@ fn generated_source_loader_parses_and_installs_module() { received_message: None, }, observer: TestObserver::default(), + transform: TransformObserver::default(), + sink: SinkObserver::default(), program: vihaco::ProgramImage::new(), }; diff --git a/demos/examples/demo/stdlib/debug_trace.rs b/demos/examples/demo/stdlib/debug_trace.rs index 6c1fd126..e26640ca 100644 --- a/demos/examples/demo/stdlib/debug_trace.rs +++ b/demos/examples/demo/stdlib/debug_trace.rs @@ -5,7 +5,7 @@ use super::{ Effects, handle::{Absorb, Observe}, }; -use vihaco::{LoadSstSubtree, SstSectionView}; +use vihaco::{LoadSstSubtree, NoEffect, SstSectionView}; vihaco::component! { component DebugTrace { @@ -61,7 +61,7 @@ where E: std::fmt::Debug, R: 'static, { - type Effect = (); + type Effect = NoEffect; type Error = std::convert::Infallible; fn observe(&mut self, effect: &E) -> Result, Self::Error> { diff --git a/docs/examples/observe.rs b/docs/examples/observe.rs index 14c30974..183f7210 100644 --- a/docs/examples/observe.rs +++ b/docs/examples/observe.rs @@ -1,5 +1,5 @@ use eyre::Result; -use vihaco::{Effects, Observe}; +use vihaco::{Effects, NoEffect, Observe}; #[derive(Debug, Clone)] pub struct Line(pub String); @@ -12,10 +12,10 @@ pub struct Collector { } impl Observe for Collector { - type Effect = (); + type Effect = NoEffect; type Error = eyre::Report; - fn observe(&mut self, effect: &Line) -> Result> { + fn observe(&mut self, effect: &Line) -> Result> { self.lines.push(effect.0.clone()); Ok(Effects::none()) } diff --git a/vision/typed-observation-trees.md b/vision/typed-observation-trees.md new file mode 100644 index 00000000..3e57cd52 --- /dev/null +++ b/vision/typed-observation-trees.md @@ -0,0 +1,458 @@ +# Typed observation trees + +## Status + +Proposal for the instruction-pipeline rewrite. + +This document describes how composite runtime routes can preserve statically +known effect types while allowing observers to emit follow-up effects. The +design is intended for any composite whose observers form a typed +fan-out/fan-in-free graph. + +## Summary + +An instruction route's `effects` block describes a statically typed effect tree. + +- Sibling `observe` entries are fan-out branches and receive the same effect. +- A nested observer block receives each concrete effect emitted by its parent + observer. +- A handler is terminal unless a later extension explicitly permits handler + follow-ups. +- No universal composite effect enum and no dynamic `DispatchEffect` trait are + required for the observation graph. + +Example: + +```rust +Produce(ProducerInstruction) => producer { + message with resolve_producer_message; + + effects { + observe transform { + observe sink; + } + + observe monitor; + } +} +``` + +The generated graph is: + +```text +ProducerEffect +├── transform → IntermediateEffect → sink +└── monitor +``` + +Every edge is checked at compile time. `sink` must implement observation for +the concrete output type of `transform`; it does not receive an erased +machine-level effect. + +## Motivation + +The current runtime contract allows an observer to return effects, but +generated composite code discards those effects. That supports observers that +only mutate state, but cannot represent workflows such as: + +```text +producer effect + → state update + → intermediate-effect construction + → terminal observer +``` + +Replacing the concrete output with a composite-wide enum would make the flow +dynamic and lose useful static knowledge. A general dispatcher would solve the +problem operationally, but would also move type selection from the composite +declaration into runtime routing. The observation tree keeps the graph in the +declaration and lets Rust type-check each edge. + +## Runtime model + +The observer trait retains its concrete associated output type: + +```rust +pub trait Observe { + type Effect; + type Error; + + fn observe(&mut self, effect: &E) -> Result, Self::Error>; +} +``` + +`Effects` remains the existing zero/one/many container. A node may emit no +follow-ups, one follow-up, or multiple follow-ups. The generated executor +iterates the returned `Effects` and evaluates the node's nested children for +each value. + +The route's component effects remain concrete. For example: + +```rust +impl Execute for Producer { + type Message = ProducerMessage; + type Effect = ProducerEffect; + type Fault = eyre::Report; + // ... +} + +impl Observe for Transform { + type Effect = IntermediateEffect; + type Error = eyre::Report; + // ... +} + +impl Observe for Sink { + type Effect = NoEffect; + type Error = eyre::Report; + // ... +} +``` + +`NoEffect` is the terminal output type for observers that intentionally emit +nothing. If an observer returns another meaningful type, it should either have +nested consumers or be explicitly discarded by syntax that makes the discard +visible. + +## Proposed syntax + +The existing route shape remains the foundation: + +```rust +runtime { + RouteName(Payload) => target { + message ...; + effects { + // observation tree and/or terminal handler + } + } +} +``` + +### Basic observer + +```rust +effects { + observe debug; +} +``` + +The observer receives the component effect produced by the route. Its output +must be terminal or explicitly discarded. + +### Nested follow-up + +```rust +effects { + observe transform { + observe sink; + } +} +``` + +The nested observer receives the concrete associated `Effect` type returned by +`transform`. + +### Fan-out + +```rust +effects { + observe transform { + observe sink; + } + + observe monitor; +} +``` + +Sibling entries receive the original route effect independently. The generated +code must not feed the output of `transform` into `monitor`. + +### Nested fan-out + +```rust +effects { + observe transform { + observe sink; + observe effect_logger; + } + + observe monitor; +} +``` + +Both nested observers receive each `IntermediateEffect` emitted by +`transform`. + +### Terminal handler + +```rust +effects { + observe event_logger { + handle with emit_log; + } +} +``` + +The handler receives the concrete output type of `event_logger`. Handlers +are terminal in the initial design and return `Result<(), Error>`. + +### Direct route handler + +```rust +effects { + observe monitor; + handle with handle_producer_effect; +} +``` + +The direct handler receives the original component effect, independently of +the observer branches. + +### Explicit discard + +If an observer intentionally produces an effect that is not consumed, make +that decision visible: + +```rust +effects { + observe metrics => discard; +} +``` + +The generated code still knows the observer's concrete output type, but does +not require a child consumer. The compiler should reject `discard` for an +observer whose output is not explicitly permitted to be dropped if the project +chooses a strict-loss policy. + +The initial implementation may instead require terminal observers to return +`NoEffect`, postponing `discard` until a concrete use case needs it. + +## Semantics + +For each route: + +1. Resolve the route message. +2. Execute the selected component instruction. +3. For every component effect, evaluate every sibling observation branch. +4. For every observer output, evaluate its nested observation branches. +5. Invoke terminal handlers at the point where their input type is known. +6. Return the route's `Execution` result to the composite's caller. + +The generated implementation is conceptually recursive, but it should not use +unbounded Rust call-stack recursion for effect values. A small internal work +stack or queue can evaluate nested nodes while preserving the declared order. +The logical order is depth-first, left-to-right: + +```text +for root effect: + branch 1 and all descendants + branch 2 and all descendants +``` + +This preserves the current declaration-order expectation for observers and +handlers while avoiding a dynamic type-erased effect queue. + +The implementation can generate typed helper functions for each node rather +than storing heterogeneous nodes in one collection. For example: + +```rust +fn observe_transform( + &mut self, + effect: &ProducerEffect, +) -> Result<(), CompositeError> { + let follow_ups = Observe::::observe( + &mut self.transform, + effect, + )?; + + for intermediate in follow_ups { + self.observe_sink(&intermediate)?; + } + + Ok(()) +} +``` + +This approach keeps the generated code monomorphic and lets the compiler +report an invalid edge at the observer declaration. + +## Macro representation + +The macro parser should represent an observation node as a tree rather than as +the current flat observer list: + +```rust +struct ObservationNode { + observer: Ident, + children: Vec, + terminal: TerminalAction, +} + +enum EffectNode { + Observe(ObservationNode), + Handle(Ident), + Discard, +} +``` + +The exact internal names are not important. The essential property is that the +parent-child relationship survives parsing and validation. + +The route validator should walk the tree with an input effect type: + +```text +validate(node, input_effect_type): + observer = observer_field(node) + output_type = >::Effect + validate(each child, output_type) +``` + +Handlers are validated against the current input effect type. Sibling nodes are +each validated against the same parent input type. + +## Route markers + +The existing route marker mechanism should be extended so each observation edge +has a stable marker. This allows one component to observe the same effect type +in different routes with different behavior: + +```rust +Observe +Observe +``` + +Nested edges may receive generated marker names derived from their path, such +as `ProducerTransformSinkRoute`. Markers should remain private implementation +details. + +## Errors and partial execution + +An observer error stops evaluation of the current route and is converted into +the composite error, just as component and handler errors are today. + +Effects already applied before the error are not rolled back. This matches the +existing mutable observer model. Documentation should state that observers +should either be order-independent or perform validation before mutating state +when transactional behavior matters. + +The generated code must not install or advance program state based on a route +until the route's message resolution and component execution have succeeded. +Observation errors occur after component execution, so the component's state +mutation is also not rolled back. + +## Cycles and resource limits + +The syntax naturally describes a tree, not a runtime graph. An observer cannot +refer to an ancestor through the declaration, so accidental cycles are +impossible in the generated observation structure. + +If a future feature allows handlers to emit follow-ups or dynamically selects +observers, it must introduce an explicit effect budget or cycle policy. That is +outside the initial design. + +## Worked example + +An example producer route could use: + +```rust +Produce(ProducerInstruction) => producer { + message with resolve_producer_message; + effects { + observe transform { + observe sink; + } + observe monitor; + } +} +``` + +An event-logging path could use: + +```rust +Event(EventInstruction) => event_source { + message with resolve_event_message; + effects { + observe event_logger { + handle with emit_log; + } + } +} +``` + +Instruction completion and program-counter policy should remain separate from +observation effects. Execution state controls whether the route completed; +observation effects describe typed side effects and follow-ups. + +## Implementation phases + +### Phase 1: Runtime contract + +- Keep `Observe::Effect` as a concrete associated type. +- Add tests covering zero, one, and many observer follow-up effects. +- Decide whether terminal observers require `NoEffect` or support explicit + `discard`. +- Document ordering and non-transactional mutation behavior. + +### Phase 2: Macro syntax and AST + +- Replace the flat observer list in route effects with an observation tree. +- Parse nested observer blocks. +- Parse terminal handler and discard actions. +- Preserve source spans for nested validation errors. + +### Phase 3: Static validation + +- Validate every observer field. +- Validate each nested edge against the parent's associated output type. +- Validate handlers against their input effect type. +- Reject observers whose output has no consumer unless they are terminally + allowed. +- Add compile-fail tests for mismatched nested observers and invalid handlers. + +### Phase 4: Code generation + +- Generate typed helper functions or typed nested blocks. +- Generate private route markers for observation edges. +- Preserve sibling fan-out and left-to-right depth-first ordering. +- Convert all observer errors into the composite error. +- Ensure no heterogeneous runtime effect container is introduced. + +### Phase 5: Integration tests + +Add tests for: + +- one observer with one follow-up; +- two sibling observers receiving the same input; +- nested fan-out; +- multiple effects emitted at one level; +- terminal `NoEffect` observers; +- explicit discard, if supported; +- error propagation from parent and nested observers; +- declaration-order guarantees; +- route-local marker specialization. + +### Phase 6: composite integration + +- Port a producer, transforming observer, and terminal observer as a reference + implementation. +- Express a typed fan-out and nested follow-up pipeline. +- Add a terminal observer/handler chain. +- Keep instruction completion and scheduling policy separate from observation + behavior. +- Add end-to-end tests that assert both machine state and observation order. + +## Non-goals + +This proposal does not introduce: + +- a universal composite effect enum; +- dynamic effect dispatch; +- runtime observer registration; +- handler follow-up effects; +- rollback or transactional observers; +- arbitrary cyclic effect graphs; +- a scheduler or event loop. + +Those features may be useful later, but they would weaken the simple static +model needed for the instruction pipeline rewrite. From 05f738ef8b72b73cb1096fb0587a1844bb05274c Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 11 Aug 2026 11:52:27 -0400 Subject: [PATCH 13/15] Added formal language specs for the `composite!` and `component!` macros --- docs/src/pages/guide/components.md | 678 +++++++++++++++++++++---- docs/src/pages/guide/composites.md | 776 ++++++++++++++++++++++++----- 2 files changed, 1228 insertions(+), 226 deletions(-) diff --git a/docs/src/pages/guide/components.md b/docs/src/pages/guide/components.md index e8a021e4..71a55e25 100644 --- a/docs/src/pages/guide/components.md +++ b/docs/src/pages/guide/components.md @@ -1,47 +1,183 @@ --- layout: ../../layouts/Guide.astro -title: Building Components +title: '`component!` Language Reference' slug: components -description: "Declare reusable runtime components with component!, define instruction products, and implement Execute per instruction." +description: "The complete language reference for the vihaco component! declaration macro." --- -# Building Components With `vihaco` +# `component!` Language Reference -A component owns state and the behavior for one or more runtime instruction -products. The component declaration and the execution implementation are two -deliberate boundaries: +This document is the normative reference for the `component!` declaration +macro. It specifies the accepted declaration grammar, generated Rust items, +runtime instruction products, optional component syntax, visibility rules, +generic behavior, and the boundaries that remain ordinary Rust. -- `component!` declares the state type and the instruction product types. -- `Execute` implements one product `I`, with its own message, effect, and - fault types. +`component!` declares a reusable component type and the product types that may +be passed to its runtime implementation. It does not define a machine-wide +instruction set. A `composite!` declaration chooses which products a machine +exposes and how messages and effects are routed. -This lets a component expose operations with different input and output -contracts without forcing them through one large instruction enum. +The macro is re-exported by `vihaco` and by `vihaco-runtime` when its `derive` +feature is enabled. -## Declare a component +## 1. Complete grammar -```rust -use vihaco::component; +The following notation describes the macro input. `Ident`, `Type`, `Expr`, +`String`, `Generics`, `WhereClause`, `Visibility`, and `Attribute` have their +Rust meanings. `ε` denotes an optional production. Whitespace, comments, and +trailing commas are accepted where the grammar permits them. +```text +Component ::= OuterAttribute* `#[module = Ident]`? Visibility? + `component` Ident Generics? WhereClause? + `{` NamedField* `}` + InstructionBlock? SyntaxBlock? + +NamedField ::= Attribute* Visibility? Ident `:` Type `,`? + +InstructionBlock + ::= `instruction` `{` Product* `}` +Product ::= OuterAttribute* Visibility? Ident ProductFields? `,`? +ProductFields ::= `(` TupleField* `)` | `{` NamedProductField* `}` +TupleField ::= Attribute* Visibility? Type `,`? +NamedProductField + ::= Attribute* Visibility? Ident `:` Type `,`? + +SyntaxBlock ::= `syntax` `{` TypeBlock ValueBlock InstructionSyntaxBlock `}` +TypeBlock ::= `type` Ident `{` SyntaxEnumVariant* `}` +ValueBlock ::= `value` Ident `{` SyntaxEnumVariant* `}` +SyntaxEnumVariant + ::= Ident `=` String (`;` | `,` | ε) +InstructionSyntaxBlock + ::= `instruction` `{` SyntaxInstructionVariant* `}` +SyntaxInstructionVariant + ::= Ident SyntaxPayload? `=` String (`;` | `,` | ε) +SyntaxPayload ::= `(` TypeList `)` +TypeList ::= Type (` ,` Type)* `,`? +``` + +The displayed comma in `TypeList` is the ordinary `,` token; spacing in the +notation is illustrative. The component state block contains named Rust +fields. Runtime products may be unit-like, tuple-like, or named-field structs. +The syntax block requires exactly one `type`, one `value`, and one +`instruction` declaration; their order within `syntax` is not significant. + +## 2. Component declaration + +The canonical declaration is: + +```rust ignore component! { - component Counter { - value: i64, + #[derive(Default)] + pub component Counter + where + T: Default, + { + value: T, } instruction { - Add(i64), - Print, + Add(T), + Reset, } } ``` -The macro creates `counter::Counter` and places the products in -`counter::instruction`: `Add(i64)` and `Print`. Named and tuple products are -also supported: +The macro generates a module, and places the component struct and product +types inside that module. With the default naming rules, the declaration above +produces `counter::Counter`, `counter::instruction::Add`, and +`counter::instruction::Reset`. -```rust -use vihaco::component; +### 2.1 Component visibility +The component's `Visibility` applies to the generated module, the component +struct, the `instruction` module, and the optional `syntax` module. If no +visibility is written, these generated items are `pub`. + +```rust ignore +component! { + pub(crate) component InternalCounter { value: i64, } +} +``` + +The generated module is the public API namespace. The macro does not emit a +component type directly at the invocation site. + +### 2.2 Attributes + +The only supported outer attribute on the component declaration is: + +```text +#[module = Ident] +``` + +It overrides the default generated module name. The value must be a single +Rust identifier: + +```rust ignore +#[module = device_cpu] +component Cpu { } +``` + +This produces `device_cpu::Cpu` instead of the default `cpu::Cpu`. The +attribute is consumed by the macro and is not emitted. Any other outer +attribute on the component declaration is rejected. + +Attributes on instruction products are preserved and emitted on the generated +product struct. They may include `#[derive(...)]`, `#[doc = ...]`, and other +attributes accepted by Rust for structs. + +Attributes on state fields and product fields are parsed as ordinary Rust +field attributes and are preserved. The macro does not assign special meaning +to them. + +### 2.3 State fields + +The first braced block is the component's state. It must contain named fields: + +```rust ignore +component! { + component RegisterFile { + values: Vec, + #[allow(dead_code)] + capacity: usize, + } +} +``` + +The generated state fields have a deliberate visibility split. A field with +no explicit visibility is emitted as `pub(super)`, allowing implementations +written in the module containing the macro invocation to access it while not +making it public to all downstream users. An explicit field visibility is +preserved. + +The component struct itself receives the declaration's generics and where +clause. State field types are resolved in the lexical scope containing the +macro invocation, so `super::Type`, local aliases, and parent-module names are +valid. + +### 2.4 Empty components + +The state block may be empty. The `instruction` block may be omitted when the +component has no products: + +```rust ignore +component! { + component Clock {} +} +``` + +This still generates `clock::Clock`. No `clock::instruction` module is +generated unless an instruction block is present. An empty `instruction {}` +block generates the instruction module with no product structs. + +## 3. Runtime instruction products + +The optional `instruction` block is a catalog of independent runtime product +types. It is not an enum and does not impose common message, effect, or fault +types. + +```rust ignore component! { component RegisterFile { values: Vec, @@ -55,51 +191,116 @@ component! { } ``` -The declaration is a catalog of runtime products. It does not define source -syntax, assign machine-wide device codes, or choose which products a composite -exposes. +The generated items are equivalent to: -## Implement `Execute` +```rust ignore +pub mod register_file { + pub struct RegisterFile { + pub(super) values: Vec, + } -Execution is implemented per product. `Message` is a marker for owned, -runtime-supplied input; `NoMessage` is the standard input for an instruction -that needs none. `StepResult` keeps returned effects separate from whether the -operation completed or parked. + pub mod instruction { + pub struct Read { + pub slot: usize, + } + pub struct Write(pub usize, pub i64); + pub struct Reset; + } +} +``` -```rust -use eyre::Result; -use vihaco::{ - component, Effects, Execute, Execution, Message, StepResult, -}; +The conceptual expansion omits generic parameters and user attributes for +brevity. -component! { - component Counter { - value: i64, - } +### 3.1 Unit products - instruction { - Add(i64), - Print, - } +```text +Reset, +``` + +generates a unit-like struct. Construct it as +`register_file::instruction::Reset`. + +### 3.2 Tuple products + +```text +Write(usize, i64), +``` + +generates a tuple struct. Tuple fields without explicit visibility are made +`pub`, because composite-generated code and downstream users must be able to +construct products: + +```rust ignore +let instruction = register_file::instruction::Write(3, 42); +``` + +Tuple field attributes and explicit visibilities are preserved. + +### 3.3 Named products + +```text +Read { slot: usize }, +``` + +generates a named-field struct. Named fields without explicit visibility are +made `pub` for the same construction boundary: + +```rust ignore +let instruction = register_file::instruction::Read { slot: 3 }; +``` + +Use explicit visibility when a product field needs a different Rust visibility. + +### 3.4 Product attributes and visibility + +The product declaration accepts outer attributes and an optional visibility: + +```rust ignore +instruction { + #[derive(Clone, Debug, PartialEq)] + pub Add(i64), + #[doc = "Stops the device."] + Reset, +} +``` + +If a product has no visibility, it is emitted as `pub`. If it has an explicit +visibility, that visibility is preserved. The product's generic parameters +are filtered from the enclosing component generics to retain only parameters +used by the product's fields and its relevant where predicates. + +### 3.5 Supported field forms + +Products support all three Rust struct forms: + +```rust ignore +instruction { + Unit, + Tuple(T, const_value_type), + Named { value: T, index: usize }, } +``` + +The macro does not generate constructors beyond Rust's normal unit, tuple, and +named struct constructors. It does not box, wrap, or convert product fields. -#[derive(Debug, Clone)] -pub struct Prefix(String); -impl Message for Prefix {} +## 4. Implementing runtime behavior -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Line(String); +`component!` declares product types but does not implement their behavior. +Implement `Execute` separately for each product and component type: +```rust ignore impl Execute for counter::Counter { - type Message = (); - type Effect = (); + type Message = NoMessage; + type Effect = NoEffect; type Fault = eyre::Report; fn execute( &mut self, instruction: &counter::instruction::Add, - _message: (), - ) -> Result, Self::Fault> { + _message: NoMessage, + ) -> Result, Self::Fault> { self.value += instruction.0; Ok(StepResult { effects: Effects::none(), @@ -107,57 +308,358 @@ impl Execute for counter::Counter { }) } } +``` -impl Execute for counter::Counter { - type Message = Prefix; - type Effect = Line; - type Fault = eyre::Report; +The runtime contract is: + +```rust ignore +trait Execute { + type Message; + type Effect; + type Fault; fn execute( &mut self, - _instruction: &counter::instruction::Print, - message: Prefix, - ) -> Result, Self::Fault> { - Ok(StepResult { - effects: Effects::one(Line(format!("{}{}", message.0, self.value))), - execution: Execution::Complete, - }) + instruction: &I, + message: Self::Message, + ) -> Result, Self::Fault>; +} +``` + +Each product may have a different `Message`, `Effect`, and `Fault`. The +component macro does not inspect, generate, or validate these implementations. +Rust trait resolution reports missing or incompatible implementations when a +composite or caller uses the product. + +### 4.1 Messages + +Messages are owned values supplied to `Execute`. Use `NoMessage` for an +operation with no input, or define a component-specific message type. The +`Message` trait is a marker for message types that need the framework's message +marker semantics; it is not required for every type used as +`Execute::Message`. + +Message acquisition is a composite concern. A composite route may pass +`NoMessage`, call `Supply` on another field, or use a composite-owned +resolver method. + +### 4.2 Effects + +`Execute::Effect` is the homogeneous item type carried by `Effects` in the +returned `StepResult`. `Effects::none()`, `Effects::one(value)`, and a many +effect stream express zero, one, or many outputs. The component chooses the +effect type; the composite chooses how to observe and consume it. + +For an operation that must not produce effects, use `NoEffect` and return an +empty effect stream. This lets generated composite routing type-check that no +output is silently discarded. + +### 4.3 Execution state + +`StepResult` contains both the effect stream and an `Execution` state: + +* `Execution::Complete` indicates that the operation completed from the + component's perspective. +* `Execution::Parked` indicates that the parent runtime must retain or + otherwise coordinate the operation before continuing. + +The component macro does not generate a program counter, event loop, timing +policy, or resume/continuation dispatch. Those remain the responsibility of +the runtime root or composite host. + +## 5. Component capabilities + +The component struct can implement capabilities independently of its products. +These implementations are ordinary Rust and are not generated: + +```text +Supply produces an owned message +Absorb consumes an owned effect +Observe borrows an effect and may emit observation effects +Handle consumes an effect for a selected route +``` + +`component!` therefore remains reusable across machines. A composite can use +one component as a target, message source, observer, or effect destination +without changing the component declaration. + +## 6. Component-local surface syntax + +The optional `syntax` block declares a component's source-language instruction, +value, and type vocabulary. It is independent of machine routing. A composite +may mount this vocabulary under one or more namespaces with its field +attribute `#[syntax(...)]`. + +The canonical form is: + +```rust ignore +component! { + component Arithmetic {} + + instruction { + Add(ArithmeticType), + } + + syntax { + type ArithmeticType { + Integer = "`integer`"; + Address = "`address`"; + } + + value ArithmeticValue { + Zero = "`zero`"; + } + + instruction { + Add(ArithmeticType) = "'add $0"; + } + } +} +``` + +The syntax block must contain all three declarations: `type`, `value`, and +`instruction`. Omitting any one is a macro expansion error. + +### 6.1 Syntax type and value enums + +`type Name { ... }` and `value Name { ... }` each generate a public enum with +the requested name. Each variant has a parser pattern: + +```text +type ArithmeticType { + Integer = "`integer`"; + Address = "`address`"; +} +``` + +is equivalent in shape to: + +```rust ignore +#[derive(Clone, Debug, PartialEq, vihaco::Parse)] +pub enum ArithmeticType { + #[pattern = "`integer`"] + Integer, + #[pattern = "`address`"] + Address, +} +``` + +Syntax type and value variants are unit variants. Their pattern strings are +passed to the parser derive and are not interpreted as runtime instruction +products. + +### 6.2 Syntax instruction enum + +The nested syntax `instruction` block generates `syntax::Instruction`. A +variant may be unit-like or carry one or more typed parser fields: + +```text +instruction { + Add(ArithmeticType) = "'add $0"; + Halt = "'halt"; +} +``` + +This generates the shape: + +```rust ignore +#[derive(Clone, Debug, PartialEq, vihaco::Parse)] +pub enum Instruction { + #[pattern = "'add $0"] + Add(ArithmeticType), + #[pattern = "'halt"] + Halt, +} +``` + +The types in the syntax instruction payload are parser-side types. They are +not automatically converted into runtime product fields and are not required +to be the same type as the component's runtime product payload. A composite or +other resolver performs that lowering. + +The pattern string is attached to the generated variant as `#[pattern = +...]`. `$0`, `$1`, and subsequent placeholders refer to the corresponding +payload fields according to the parser derive's pattern rules. + +### 6.3 Syntax module and `InstructionSet` + +When `syntax` is present, the generated component module contains: + +* `syntax::` — the declared type enum; +* `syntax::` — the declared value enum; and +* `syntax::Instruction` — the generated instruction enum. + +The component type implements: + +```rust ignore +impl InstructionSet for arithmetic::Arithmetic { + type Instruction = arithmetic::syntax::Instruction; + type Value = arithmetic::syntax::ArithmeticValue; + type Type = arithmetic::syntax::ArithmeticType; +} +``` + +The `InstructionSet` implementation is what allows a composite to mount the +component with `#[syntax]`. It does not assign a namespace; namespaces belong +to the composite's mount. + +All generated syntax enums derive `Clone`, `Debug`, and `PartialEq`, and use +the framework's parser derive. Their visibility follows the component's +visibility. + +## 7. Generated API summary + +For: + +```rust ignore +component! { + pub component GateBeam where T: Clone { + state: T, + } + + instruction { + Measure(T), + Reset, + } +} +``` + +the generated public shape is: + +```text +gate_beam::GateBeam +gate_beam::instruction::Measure +gate_beam::instruction::Reset +``` + +More precisely: + +* the module is `gate_beam` by default, or the identifier supplied by + `#[module = ...]`; +* the component struct is `::GateBeam`; +* products are `::instruction::`; +* state fields without explicit visibility are `pub(super)`; +* product fields without explicit visibility are `pub`; +* a syntax block adds `::syntax`, the declared enums, and + `InstructionSet` for the component; and +* the macro emits no runtime execution method or component-wide instruction + enum. + +The generated module uses `use super::*`, so types in the parent module are +available to generated state, product, and syntax declarations. Product and +component generic parameters are retained according to where they are used; +unused enclosing generics are not copied onto individual products or syntax +types. + +## 8. Generic and const-generic components + +The component declaration accepts Rust generics and an optional where-clause: + +```rust ignore +component! { + component GenericComponent + where + T: Clone, + { + value: T, + } + + instruction { + Unit, + Tuple(T), + Array([T; N]), } } ``` -The `Execute` contract is: +The component is `generic_component::GenericComponent`. The products +are: ```text -Execute::execute(&mut self, &I, Message) - -> Result, Fault> +generic_component::instruction::Unit +generic_component::instruction::Tuple +generic_component::instruction::Array ``` -`Effects` can contain zero, one, or many values. `Execution::Complete` tells a -parent that it may advance its program counter; `Execution::Parked` tells it -to retain the operation until an owned completion is available. The runtime -does not infer timing or scheduling from this value. +`Unit` does not retain `T` or `N`, `Tuple` retains `T`, and `Array` retains +both. Relevant where predicates are retained with the parameters they +constrain. This allows a product to be used independently of unrelated state +or component parameters. + +Generic types used only in state remain on the component struct but do not +appear on products that do not reference them. + +## 9. Naming and validation -## Capabilities around execution +The macro validates generated names during expansion. It rejects: -Components can expose reusable capabilities independently of instruction -execution: +* unsupported component-level attributes; +* invalid generated module names; +* duplicate `#[module]` attributes; +* duplicate product names after snake-case normalization; +* syntax blocks missing `type`, `value`, or `instruction`; +* malformed state or product field declarations; and +* trailing tokens after the final declaration block. -- `Supply` produces an owned message, often from a stack or queue. -- `Absorb` consumes an owned effect, often by updating state. -- `Observe` borrows an effect for diagnostics, tracing, or recording. -- `Handle` is the composite-selected route for the one consumer that - receives ownership of an effect. +Product name collision checking is performed after removing a raw-identifier +prefix and converting the name to snake case. For example, two product names +that normalize to the same generated name are rejected. Rust then performs the +remaining semantic checks, including duplicate fields, invalid visibility, +generic bounds, and derive errors from product attributes. -These contracts keep a reusable component independent of the composite that -contains it. The composite decides which capability is used on each route. +The macro does not validate whether a product has an `Execute` implementation, +whether a message/effect type is suitable, or whether a syntax pattern lowers +to a runtime product. Those are intentionally separate component and composite +contracts. -## Planned extensions +## 10. What `component!` does not generate + +The following remain author- or composite-defined Rust: + +* `Execute` implementations; +* a component-wide instruction enum; +* source namespaces and machine-wide syntax selection; +* runtime route selection; +* message supply and message resolution; +* effect observation and consumption policy; +* opcodes, widths, bytecode encoders, and decoders; +* program counters and instruction fetching; +* scheduling, timing, parking, and resume policy; and +* a universal machine execution trait. + +If a single encoded instruction enum is required, define it explicitly or use +`#[derive(Instruction)]` on an appropriate enum. The independent product types +generated by `component!` are the normal inputs to `Execute` and to +composite route declarations. + +## 11. Integration with `composite!` + +A composite selects component products explicitly: + +```rust ignore +composite! { + composite Machine { + error = MachineError; + + #[syntax("arithmetic")] + arithmetic: arithmetic::Arithmetic, + } + + runtime { + Add(arithmetic::instruction::Add) => arithmetic { + message none; + } + } +} +``` -The current runtime leaves resume/continuation dispatch and timing policy to -ordinary Rust in the parent runtime. A future macro layer is planned to make -those boundaries more convenient; until then, examples should implement them -explicitly and should treat `Execution::Parked` as a real runtime state. +The component owns `arithmetic::instruction::Add`, its state, its +`Execute` implementation, and optionally its local parser vocabulary. +The composite owns the public machine route, source namespace, message policy, +and effect policy. A component product may be selected by multiple composites +or by multiple routes, and one product type may be executed by multiple +component state types when their `Execute` implementations permit it. -Continue with [Defining Composites](/guide/composites) and -[Using Messages](/guide/messages). +For route grammar and generated machine behavior, see the +[`composite!` Language Reference](/guide/composites). For encoding an enum, +see [Defining Instructions](/guide/instructions). diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index 7484120f..ef874725 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -1,76 +1,100 @@ --- layout: ../../layouts/Guide.astro -title: Defining a Composite +title: '`composite!` Language Reference' slug: composites -description: "Compose components with composite!, select runtime routes, resolve messages, and deliver effects." +description: "The complete language reference for the vihaco composite! declaration macro." --- -# Defining a Composite With `vihaco` +# `composite!` Language Reference -A composite is the machine-specific composition root. It owns component -instances and declares the routes that connect a public machine instruction to -a component product, a message source, observers, and one effect handler. +This document is the normative reference for the `composite!` declaration +macro. It describes the accepted token grammar, the meaning of each clause, +the generated Rust items, and the trait contracts required of the types named +by a declaration. It assumes familiarity with Rust, procedural macros, and the +runtime traits in `vihaco`. -## A routed composite +The macro is re-exported by both `vihaco` and `vihaco-runtime` (when the +runtime crate's `derive` feature is enabled). It is invoked as a function-like +macro: -```rust -use eyre::Result; -use vihaco::{ - composite, Absorb, Effects, Execute, Execution, Message, Observe, StepResult, - Supply, -}; +```text +composite! { composite-declaration } +``` -struct Stack(Vec); -impl Supply<(i64, i64)> for Stack { - type Fault = eyre::Report; - fn supply(&mut self) -> Result<(i64, i64), Self::Fault> { - let rhs = self.0.pop().ok_or_else(|| eyre::eyre!("underflow"))?; - let lhs = self.0.pop().ok_or_else(|| eyre::eyre!("underflow"))?; - Ok((lhs, rhs)) - } -} +The declaration owns a machine's composition policy. Components remain +ordinary Rust values. A composite chooses which component receives each +runtime instruction, where its message comes from, which observers see its +effects, and which handler consumes those effects. -#[derive(Clone)] -struct Add; -struct Arithmetic; -struct Value(i64); -impl Execute for Arithmetic { - type Message = (i64, i64); - type Effect = Value; - type Fault = eyre::Report; - fn execute(&mut self, _: &Add, (lhs, rhs): (i64, i64)) -> Result> { - Ok(StepResult { - effects: Effects::one(Value(lhs + rhs)), - execution: Execution::Complete, - }) - } -} -impl Absorb for Stack { - type Fault = eyre::Report; - fn absorb(&mut self, value: Value) -> Result<()> { self.0.push(value.0); Ok(()) } -} -struct Trace; -impl Observe for Trace { - type Effect = (); - type Error = eyre::Report; - fn observe(&mut self, _: &Value) -> Result> { Ok(Effects::none()) } -} +## 1. Complete grammar -composite! { - composite Calculator { - error = eyre::Report; +The following is a notation for the macro grammar. `Ident`, `Type`, `Expr`, +`String`, `Integer`, and `Attribute` have their Rust meanings. `ε` means that +the production is optional. Whitespace and Rust comments may occur wherever +Rust permits them. + +```text +Composite ::= OuterAttribute* Visibility? `composite` Ident Generics? + WhereClause? `{` ErrorClause? Field* `}` + SyntaxBlock? RuntimeBlock? + +ErrorClause ::= `error` `=` Type `;` + +Field ::= Attribute* Visibility? Ident `:` Type + +SyntaxBlock ::= `syntax` `{` HeaderClause? SyntaxEntry* `}` +HeaderClause ::= `header` Type `=>` Ident `;` +SyntaxEntry ::= `#[pattern = String]` Ident Payload? `=>` + (`runtime` Ident | Ident) `;` +Payload ::= `(` Type `)` + +RuntimeBlock ::= `runtime` `{` Route* `}` +Route ::= Ident `(` Type `)` `=>` Ident `{` MessageClause EffectsBlock? `}` +MessageClause ::= `message` (`none` | `from` Ident | `with` Ident) `;` +EffectsBlock ::= `effects` `{` EffectDeclaration* `}` +EffectDeclaration + ::= ObserveDeclaration + | `absorb` `with` Ident `;` + | `handle` `with` Ident `;` +ObserveDeclaration + ::= `observe` Observer (` ,` Observer)* (`;` | ε) +Observer ::= Ident (`{` EffectDeclaration* `}`)? +``` - #[device(0x01, alias = "alu")] - arithmetic: Arithmetic, +The displayed grammar uses a comma with optional surrounding whitespace in +`Observer`; the actual token is simply `,`. Fields are parsed using Rust's +named-field grammar and are comma-terminated, so a trailing comma is allowed. +The `runtime` block may contain routes separated by whitespace or commas; a +trailing comma is allowed. An `effects` block may contain multiple observer +declarations and at most one handler. + +The grammar is deliberately narrower than Rust in a few places. In +particular, a field must be named, `error` must be the first item in the +struct body when present, every route must have exactly one message clause, +and `#[pattern = ...]` is the only accepted attribute on a syntax entry. + +## 2. Declaration and generated items + +The canonical shape is: + +```rust ignore +composite! { + #[derive(Default)] + pub composite Machine + where + T: Default, + { + error = MachineError; + + #[device(0x01, alias = "cpu")] + cpu: Cpu, stack: Stack, - trace: Trace, } runtime { - Add(Add) => arithmetic { + Run(RunInstruction) => cpu { message from stack; effects { - observe trace; absorb with stack; } } @@ -78,140 +102,616 @@ composite! { } ``` -The generated public `CalculatorInstruction::Add(Add)` is the machine-local -runtime sum. `execute_generated` resolves the message, calls -`Execute`, invokes observers in declaration order, and passes each effect -to exactly one handler. `absorb with stack` delegates to `Stack::absorb`; use -`handle with method` when routing policy belongs to the composite. +The macro emits, in the scope of the invocation: + +* the declared struct, with the supplied visibility, outer attributes, fields, + generics, and where-clause; +* `Instruction`, if the runtime block is non-empty; +* a private route-marker module and one private marker type per route; +* an optional snake-case module named after the composite, for example + `machine` for `Machine`; +* `GeneratedMachine` metadata implementation; +* generated SST child-loading support for `#[loadable]` fields; and +* inherent methods for dispatch, surface lowering, and program loading when + the corresponding declarations are present. + +The macro removes the declaration-only field attributes `#[device]`, +`#[loadable]`, `#[program]`, and `#[syntax]` before emitting the struct. Other +field attributes are preserved and are interpreted by Rust normally. The +macro also removes a top-level `#[vihaco(...)]` crate override after using it +to resolve generated paths. + +### 2.1 Composite visibility, attributes, and generics + +`Visibility` applies to the generated struct. Outer attributes apply to the +struct; this is the normal place for `#[derive(...)]`, `#[allow(...)]`, and +documentation attributes. The declaration accepts Rust type, lifetime, and +const generics and an optional where-clause. Generated enums and helper items +retain only the generic parameters that occur in their payloads or syntax +payloads. The struct and its trait implementations retain the declaration's +generics. + +The optional crate override has the form: + +```rust ignore +#[vihaco(crate = ::my_framework)] +composite Machine { /* ... */ } +``` + +It is useful when the facade is renamed or generated code must use a specific +runtime path. + +### 2.2 `error = Type` -## Route clauses +`error = Type;` declares the error type used by executable composites. It is +required when at least one runtime route exists and forbidden only by +omission—not by an explicit rule—when a structural composite has no routes. +All failures at the generated execution boundary are converted with +`Into`: -Every route names a payload and target: +* message supply and message resolver failures; +* target `Execute` failures; +* observer failures; +* `Absorb` failures; and +* `handle with` method failures. + +The generated execution method returns `Result`. The +macro does not require a particular error library; `eyre::Report` is common in +the workspace. + +### 2.3 Fields + +Fields are ordinary named Rust struct fields. Their types are used directly; +the macro does not wrap, clone, or otherwise transform them. A route target, +message source, observer, or absorb destination refers to a field by its +identifier. + +The following field attributes are consumed by the macro. + +#### `#[device(code, alias = "name", ...)]` + +Marks a field as a machine device. `code` is a decimal Rust integer literal +that must fit in `u8`. The only supported optional argument is one or more +`alias = "..."` arguments: + +```rust ignore +#[device(0x01, alias = "cpu", alias = "host")] +cpu: Cpu, +``` + +The implementation accepts integer literals such as `1` and `0x01` that +`syn` can parse as `u8`. Device codes must be unique. A device field's Rust +identifier and each alias become source symbols in composite metadata; all +source-symbol names must be unique across devices and aliases. + +Device metadata is independent of runtime routes. A device may be declared +without a route, and a route target need not be marked as a device. + +#### `#[syntax]` and `#[syntax = "namespace"]` + +Marks a device/component field as a mounted component syntax namespace. The +attribute without arguments uses the field name as the namespace. A name-value +form supplies one namespace, and a list supplies one or more aliases: + +```rust ignore +#[syntax] +left: Cpu, +#[syntax("right", "secondary")] +right: Cpu, +``` + +Every namespace must be a valid Rust identifier and must be unique. The field +type must implement `InstructionSet`; its associated `Instruction`, `Value`, +and `Type` become variants in the generated surface syntax enums. A mounted +field need not have a runtime route, but component syntax lowering methods are +generated only when runtime routes exist. + +#### `#[loadable]` and `#[loadable = "section/name"]` + +Marks a device field as a direct child of the composite's SST section. Bare +`#[loadable]` uses the field identifier as the section name. The name-value +form supplies the section's local name: + +```rust ignore +#[device(0x01)] +#[loadable = "cpu-a"] +cpu: Cpu, +``` + +The name must be non-empty and must not contain `/`. A loadable field must also +be a device. Names must be unique. The generated loader requires the field +type to implement `LoadSstSubtree`. + +#### `#[program]` + +Marks the one field that owns the composite's program module. At most one +field may be marked. The field type participates in generated `resolve_parsed`, +`load_parsed`, and `load_source` methods and must implement the relevant +`BuildProgramModule` and `InstallProgramModule` contracts. The +standard type is `ProgramImage`. + +`#[program]` does not imply `#[device]` or `#[loadable]`; program ownership and +device/SST-tree ownership are separate concerns. + +## 3. Runtime routes + +The runtime block declares the machine-local instruction set and execution +dispatch. Each route has this form: ```text -Variant(Payload) => field { - message none; - effects { - observe observer_a, observer_b; - handle with composite_method; +VariantName(PayloadType) => target_field { + message ...; + effects { ... } +} +``` + +`VariantName` must be unique in the runtime block. `PayloadType` is passed +unchanged to `Execute` and becomes the payload of the generated +instruction enum variant. The target field must exist. There is no implicit +conversion between payload types and no component-wide instruction enum +inferred by the macro. + +### 3.1 Generated runtime instruction enum + +For routes `Add(AddInstruction) => alu` and `Reset(ResetInstruction) => alu`, +the macro emits the public enum: + +```rust ignore +pub enum MachineInstruction { + Add(AddInstruction), + Reset(ResetInstruction), +} +``` + +Construct instructions as ordinary Rust values: + +```rust ignore +let instruction = MachineInstruction::Add(AddInstruction { /* ... */ }); +let execution = machine.execute_generated(&instruction)?; +``` + +The enum derives `Clone`, but not `Debug`, `PartialEq`, or encoding traits. Its +generic parameters are limited to those used by route payloads. The enum is +public even though dispatch internals are private, so a runtime root, resolver, +or program container can construct it. + +### 3.2 Message clauses + +Every route requires exactly one message clause. + +#### `message none;` + +The target's associated message type must be `NoMessage` (or otherwise satisfy +the exact type required by the `Execute` implementation), and the generated +call passes `NoMessage` without reading another field. + +#### `message from field;` + +The target's associated message type is inferred as +`>::Message`. The source field must implement: + +```rust ignore +Supply +``` + +The generated dispatch calls `Supply::supply(&mut self.field)` and owns the +returned message before invoking the target. This is important for parked +execution: the target cannot retain a borrow into the composite through the +message path. + +#### `message with method;` + +The composite must implement the generated message-resolver trait method. The +method receives a shared reference to the route payload and returns the target +message: + +```rust ignore +impl MachineMessageResolver for Machine { + fn resolve_add( + &mut self, + instruction: &AddInstruction, + ) -> Result<>::Message, MachineError> { + // Read composite state and construct the owned message. + todo!() } } ``` -Message sources are deliberately explicit: +The trait is also available as `machine::runtime::MessageResolver`; the facade +re-exports it as `MachineMessageResolver` when routes exist. The method name is +not checked until normal Rust trait resolution, so a missing implementation is +a compiler error. -- `message none` passes `NoMessage`. -- `message from field` calls `Supply` on that field. -- `message with method` calls the generated message-resolver trait with the instruction payload. +### 3.3 Effects and handlers -Effect handlers are exclusive: +An `effects` block is optional. An effect-producing route normally declares +one handler and may declare observers: -- `absorb with field` calls `Absorb` on a component field. -- `handle with method` calls a composite method with owned `E`. +```text +effects { + observe trace, metrics; + absorb with stack; +} +``` -The declared `error = E` type is the normalization boundary for component, -message, observer, and handler failures. +There may be at most one handler per route. The alternatives are exclusive. -## Devices and loading +#### `absorb with field;` -`#[device(code, alias = "name")]` contributes device metadata and source-symbol -aliases. Codes must be unique. `#[loadable]` marks a device that receives a -direct child SST section through the generated loader. A composite that owns -program data implements `LoadSstProgram` in ordinary Rust. +The destination field must implement `Absorb`, where: -The composite macro can also declare structural composites with no -`runtime` block. Those composites still provide fields, device -metadata, and section wiring, while their event loop or parent dispatch remains -hand-written. +```text +E = >::Effect +``` + +For each effect returned by the target, the generated handler invokes +`Absorb::absorb(&mut self.field, effect)`. The effect is moved into the +destination exactly once. -## Surface syntax and program loading +#### `handle with method;` -An executable composite can own the source grammar for its machine program. -The `syntax` block declares surface instructions, while the `runtime` block -declares the runtime routes they lower to: +The composite must provide an inherent method with the owned effect as its +only argument. Its return error must convert into the composite error: ```rust ignore -composite Machine { - error = eyre::Report; +impl Machine { + fn handle_output(&mut self, effect: Output) -> Result<(), MachineError> { + // Composite-owned policy. + Ok(()) + } +} +``` - #[device(0x01)] - cpu: Cpu, +The generated call does not pass the route marker or instruction. Those are +implementation details of dispatch. - #[program] - program: ProgramImage, +#### Routes without handlers + +An effects block may be omitted, or may contain observers without a terminal +handler. A terminal generated observation path must produce `NoEffect`; if no +observer exists, the route's `Execute::Effect` is type-checked directly as +`NoEffect`. This prevents an effect stream from being silently discarded. + +### 3.4 Observers + +An observer is a named field implementing: + +```rust ignore +impl Observe for Observer { + type Effect = ObserverEffect; + type Error = ObserverError; + + fn observe( + &mut self, + effect: &InputEffect, + ) -> Result, Self::Error> { + todo!() + } +} +``` + +The route marker is private and unique to the route. The public way to select +an observer behavior is therefore to implement `Observe` for the generated +marker through the route's expansion; users name the observer field only in +the declaration. + +Observers run in declaration order. They borrow the incoming effect; they do +not consume or clone it. Each observer's returned effects may be handled by a +nested observer tree: + +```text +effects { + observe trace { + observe trace_sink; + absorb with log; + } + absorb with stack; +} +``` + +The outer observer sees the target effect. The nested observer sees each +effect emitted by the outer observer. A nested terminal observer with no +handler must emit `NoEffect`; a nested `absorb` or `handle` consumes each +emitted effect. Observer failures and nested-handler failures are normalized +into the composite error. + +The same field cannot appear twice at the same observer-tree level. The +implementation permits the same field at different nesting levels. + +## 4. Generated execution behavior + +For every route, the macro generates an inherent method with the effective +signature: + +```rust ignore +fn execute_generated( + &mut self, + instruction: &MachineInstruction, +) -> Result +``` + +It is private Rust visibility. Code in the module containing the macro +invocation can call it directly; an external public API should expose its own +wrapper if it needs to execute a composite from another module. + +The route algorithm is, conceptually: + +```text +match instruction { + Route(payload) => { + message = resolve the route's message; + result = target.execute(&mut target, payload, message)?; + for effect in result.effects { + run observers in declaration order; + pass effect to the route handler, if any; + } + return result.execution; + } +} +``` + +The target is borrowed mutably only for its `Execute` call. Effects are then +processed in the returned `Effects` stream. `Execution` is returned unchanged; +the macro does not fetch instructions, advance a program counter, schedule +events, or implement resume/continuation policy. + +The runtime contracts involved are: + +```rust ignore +trait Execute { + type Message; + type Effect; + type Fault; + fn execute(&mut self, instruction: &I, message: Self::Message) + -> Result, Self::Fault>; } +``` + +The remaining contracts are `Supply`, `Observe`, `Absorb`, and +`Handle`. The macro generates private `Handle` +implementations for route handlers; authors normally interact with handlers +through the declaration rather than naming those marker types. + +## 5. Surface syntax block +The `syntax` block is optional. It adds a composite-owned source-language +layer. It can coexist with mounted component syntax from `#[syntax]` fields. + +### 5.1 Composite-owned entries + +An entry has a Chumsky/parser pattern, a public surface variant, an optional +payload, and either a direct runtime mapping or a named lowerer: + +```rust ignore syntax { #[pattern = "'machine::halt"] Halt => runtime Halt; + #[pattern = "'machine::load $0"] Load(u64) => lower_load; } - -runtime { - Halt(Halt) => cpu { message none; } - LoadConstant(u64) => cpu { message none; } -} ``` -Direct `runtime` mappings are intended for unit surface instructions. A named -lowerer handles payloads and may expand one surface instruction into several -runtime instructions: +The pattern is a string literal consumed by `#[derive(Parse)]` machinery. +Patterns must be unique. Surface variant names must be unique within the +composite syntax enum. + +`=> runtime Route` is allowed only for a unit surface variant. `Route` must be +the name of an existing runtime route. The generated lowering returns a +single runtime instruction containing the route's payload type; therefore a +unit direct mapping is appropriate only when the runtime payload can be +constructed as a unit value. + +`=> lowerer` requires a payload. The composite must implement the generated +resolver method: ```rust ignore impl machine::syntax::Resolver for Machine { fn lower_load( &mut self, value: u64, - ) -> Result> { - Ok(vec![machine::runtime::Instruction::LoadConstant(value)]) + ) -> Result, MachineError> { + Ok(vec![machine::runtime::Instruction::Load( + LoadInstruction(value), + )]) } } ``` -The generated parser is available as -`machine::syntax::Instruction::parser()`. A parsed module can be resolved and -installed with an explicit context: +The lowerer may return zero, one, or many runtime instructions. Its error is +converted into the composite error. -```rust ignore -let parsed = machine::syntax::ParsedModule::parse_section(section)?; -machine.load_parsed(parsed, ContextHandle::new(MachineContext))?; +### 5.2 Header declaration + +```text +syntax { + header HeaderType => resolve_header; + ... +} ``` -For an SST section, provide the surface-type and header types explicitly: +The generated `syntax::Header` is an alias for `HeaderType`, and the generated +resolver trait requires `resolve_header(HeaderType) -> Result<(), Error>`. +When no header clause is present, `syntax::Header` is a generated unit-like +header that implements `FromText` and `SstHeader`. A public +`syntax::parse_header(section)` helper is generated only when a header clause +is present. -```rust ignore -machine.load_source::(section)?; +### 5.3 Mounted component syntax + +For each field marked `#[syntax(...)]`, the generated `machine::syntax` +module contains: + +```text +Instruction::FieldName(ComponentInstruction) +Value::FieldName(ComponentValue) +Type::FieldName(ComponentType) +``` + +`FieldName` is the field identifier converted to PascalCase. The parser accepts +each namespace and alias as a prefix, such as `left::step 7`. The component +type must implement `InstructionSet`. + +When the composite has runtime routes, the generated resolver trait also +contains `lower_` for each mounted field. Implementing it lowers the +component instruction into `Vec`. + +### 5.4 Generated syntax API + +If a syntax block, syntax mount, or header is present, the macro generates the +snake-case module named after the composite. Its important public items are: + +* `syntax::Instruction`, implementing `Parse` and `SurfaceInstruction`; +* `syntax::Value`, implementing `Parse`; +* `syntax::Type`, implementing `Parse`; +* `syntax::Header`; +* `syntax::Module`, implementing `ModuleSyntax`; and +* `syntax::Resolver`, the trait implemented by the composite author. + +The module's `runtime::Instruction` is an alias of `Instruction` when +runtime routes exist. At the parent scope, the macro re-exports +`syntax::Instruction` as `SurfaceInstruction`, `syntax::Resolver` as +`SyntaxResolver`, and the runtime message resolver as +`MessageResolver` where applicable. + +## 6. Program construction and SST loading + +Program loading is generated only when all of the following are true: + +1. the composite is executable (`error` and routes are present); +2. it has surface syntax (a syntax entry or syntax mount); and +3. one field is marked `#[program]`. + +The program field's type controls storage through `BuildProgramModule`. The +standard `ProgramImage` implementation stores a `LocalModule`, context, and +program counter, but the macro does not require that representation. + +### 6.1 `resolve_parsed` + +The generated method has the effective shape: + +```text +resolve_parsed( + &mut self, + ParsedModule, +) -> Result +``` + +It resolves an optional header, creates an empty module, interns strings, +copies constants and source symbols, lowers every function instruction, records +function and label metadata, selects a function named `main`, and calls +`BuildProgramModule::finish`. Lowering diagnostics identify the function, +instruction index, and source instruction. + +The surface syntax type must convert into the program builder's associated +`Type` type. The program builder's associated instruction type must be the +generated runtime instruction enum. + +### 6.2 `load_parsed` + +`load_parsed(parsed, context)` calls `resolve_parsed` and installs the resulting +module through `InstallProgramModule`. A successful standard +`ProgramImage` installation replaces its module and context and resets `pc` to +zero. Installation is delegated to the program container, so custom +containers define their own atomicity and storage policy. + +### 6.3 `load_source` + +`load_source(section)` parses an SST section using the generated syntax module, +resolves it, validates and forwards direct loadable child sections, and +installs the resulting module using the section's context handle. Every +`#[loadable]` field must have a matching child section; duplicate, unexpected, +root-level, or missing child names are errors. + +The composite itself must implement `LoadSstProgram` for the +composite section. This hook is invoked before generated child forwarding. A +structural or executable composite can therefore keep composite-owned section +behavior explicit in ordinary Rust. + +For composites with loadable children, the macro also generates: + +```text +load_generated_sst_children(section) -> Result<()> ``` -`load_parsed` constructs a fresh module, lowers every function, records -function metadata, selects `main`, installs the module and context, and resets -the program counter. Malformed input or a lowering failure returns an error. +and a `LoadSstSubtree` implementation for the composite. The latter +loads the composite program hook and is what lets a parent composite forward a +child subtree to it. -## Custom program containers +## 7. Metadata and structural composites -`ProgramImage` is the standard program container. A composite author only -needs to mark its program field with `#[program]`. A library author who needs -custom storage or metadata can implement `BuildProgramModule` and -`InstallProgramModule` for another container. The builder controls module -creation, instruction appending, string interning, function metadata, -constants, and final validation; generated `load_parsed` uses those operations -without depending on `LocalModule` directly. +`GeneratedMachine` is implemented for every expansion. Its `metadata()` method +returns `CompositeMetadata` containing static slices of: -This keeps source resolution independent from the representation used by a -particular host VM. +* `DeviceMetadata { code, name }` for every `#[device]` field; and +* `SourceSymbolAliasMetadata { name, device_code }` for every device alias. -## Runtime boundaries +The `CompositeMetadata` helpers support device lookup, source-symbol-to-device +resolution, and validation of module source symbols. Device field names are +available through `device_by_name` and are also valid source symbols. -The macro does not fetch instructions, own a program counter, generate a clock, -or generate continuation/resume dispatch. A runtime root can call -`execute_generated`, inspect `Execution`, update its own program state, and -schedule the next owned event. The demo shows this pattern with a CPU child and -a global event loop. +A declaration with no `runtime` block is a structural composite: -Those conveniences are planned for a later API extension. Documentation and -examples that need timing or parked operations should continue to show the -explicit parent-owned loop until that extension is implemented. +```rust ignore +composite! { + pub composite Fabric { + clock: GlobalClock, + #[device(0x01, alias = "cpu-a")] + cpu_a: Cpu, + #[device(0x02, alias = "cpu-b")] + cpu_b: Cpu, + } +} +``` -See [Building Components](/guide/components), [Using Messages](/guide/messages), -and [Observing Effects](/guide/observers) for the individual contracts. +It generates the struct, device metadata, and any generated section wiring, +but no runtime instruction enum, route dispatch, message-resolver trait, or +`execute_generated`. Scheduling, child selection, timing, continuation, and +deadlock policy remain ordinary Rust. + +## 8. Validation and errors + +Expansion-time validation rejects: + +* non-named fields; +* duplicate device codes, aliases, source symbols, or loadable names; +* a loadable field without a device; +* invalid loadable names; +* multiple `#[program]` fields; +* duplicate syntax variants or patterns; +* invalid or duplicate syntax namespaces; +* direct syntax mappings with payloads; +* unknown direct runtime routes; +* named syntax lowerers without payloads; +* duplicate runtime route variants; +* unknown route targets or message-source fields; +* duplicate message clauses or effects blocks; +* missing message clauses; +* duplicate observers at one tree level; +* unknown observer or absorb fields; and +* duplicate route handlers. + +Rust type checking then enforces the semantic contracts: `Execute` on every +target/payload pair, `Supply` for `message from`, resolver methods for +`message with`, `Observe` for every observer, `Absorb` for absorb handlers, +the signatures of composite-owned methods, program-builder/installer bounds, +and all required `Into` conversions. + +## 9. Public integration boundary + +The stable author-facing integration points are the declared struct, the +generated `Instruction`, the generated snake-case syntax/runtime modules, +the resolver traits, `CompositeMetadata`, and the generated program-loading +methods when enabled. Route marker types and the `Handle` implementations are +private implementation details. + +The macro intentionally does not define a universal machine execution trait. +A runtime root generally owns the fetch/step loop and calls +`execute_generated`, then interprets `Execution` and applies its own program, +clock, or scheduling policy. This separation allows the same composite +declaration to be embedded in different host runtimes. + +For the component-side contracts, see [Building Components](/guide/components). +For message and effect semantics, see [Using Messages](/guide/messages) and +[Observing Effects](/guide/observers). The parser-specific examples are in +[Advanced Parser Integration](/guide/parser-advanced). From 9843d9320ebfe31dd20677e8af05ddb51c666ede Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Tue, 11 Aug 2026 15:03:18 -0400 Subject: [PATCH 14/15] Update `component!` macro to allow for types and values to be named and tuples --- crates/vihaco-runtime-derive/src/component.rs | 60 +++++++++++++++-- crates/vihaco/tests/component_macro.rs | 64 +++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/crates/vihaco-runtime-derive/src/component.rs b/crates/vihaco-runtime-derive/src/component.rs index a65ed2f7..809591de 100644 --- a/crates/vihaco-runtime-derive/src/component.rs +++ b/crates/vihaco-runtime-derive/src/component.rs @@ -38,7 +38,8 @@ struct SyntaxEnum { struct SyntaxVariant { name: Ident, - pattern: syn::LitStr, + fields: Fields, + pattern: Option, } struct SyntaxInstruction { @@ -159,9 +160,14 @@ fn parse_syntax_enum(input: ParseStream<'_>) -> Result { while !content.is_empty() { let variant = SyntaxVariant { name: content.parse()?, + fields: parse_variant_fields(&content)?, pattern: { - content.parse::()?; - content.parse()? + if content.peek(Token![=]) { + content.parse::()?; + Some(content.parse()?) + } else { + None + } }, }; variants.push(variant); @@ -174,6 +180,32 @@ fn parse_syntax_enum(input: ParseStream<'_>) -> Result { Ok(SyntaxEnum { name, variants }) } +fn parse_variant_fields(input: ParseStream<'_>) -> Result { + if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + Ok(Fields::Unnamed(syn::FieldsUnnamed { + paren_token: Default::default(), + unnamed: syn::punctuated::Punctuated::::parse_terminated_with( + &content, + Field::parse_unnamed, + )?, + })) + } else if input.peek(syn::token::Brace) { + let content; + syn::braced!(content in input); + Ok(Fields::Named(syn::FieldsNamed { + brace_token: Default::default(), + named: syn::punctuated::Punctuated::::parse_terminated_with( + &content, + Field::parse_named, + )?, + })) + } else { + Ok(Fields::Unit) + } +} + fn parse_syntax_instruction_variants( input: ParseStream<'_>, ) -> Result> { @@ -430,18 +462,32 @@ pub fn expand(input: TokenStream) -> TokenStream { let value_name = value_declaration.name; let type_variants = type_declaration.variants.into_iter().map(|variant| { let name = variant.name; + let fields = variant.fields; let pattern = variant.pattern; + let fields = match fields { + Fields::Unit => quote! {}, + Fields::Named(fields) => quote! { #fields }, + Fields::Unnamed(fields) => quote! { #fields }, + }; + let pattern = pattern.map(|pattern| quote! { #[pattern = #pattern] }); quote! { - #[pattern = #pattern] - #name + #pattern + #name #fields } }); let value_variants = value_declaration.variants.into_iter().map(|variant| { let name = variant.name; + let fields = variant.fields; let pattern = variant.pattern; + let fields = match fields { + Fields::Unit => quote! {}, + Fields::Named(fields) => quote! { #fields }, + Fields::Unnamed(fields) => quote! { #fields }, + }; + let pattern = pattern.map(|pattern| quote! { #[pattern = #pattern] }); quote! { - #[pattern = #pattern] - #name + #pattern + #name #fields } }); let instruction_variants = instruction_declaration.variants.into_iter().map(|variant| { diff --git a/crates/vihaco/tests/component_macro.rs b/crates/vihaco/tests/component_macro.rs index 791ad11b..ba695a7e 100644 --- a/crates/vihaco/tests/component_macro.rs +++ b/crates/vihaco/tests/component_macro.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT use vihaco::component; +use vihaco_parser::{BareToken, QuotedString}; pub struct ParentContext; @@ -35,6 +36,31 @@ component! { } } +component! { + component SyntaxComponent { + state: u8, + } + + instruction { + Runtime(u32), + } + + syntax { + type SyntaxType { + U32 = "`u32`"; + } + + value SyntaxValue { + Quoted(QuotedString), + Bare(BareToken), + } + + instruction { + Runtime(SyntaxValue) = "'runtime $0"; + } + } +} + #[test] fn components_without_instructions_still_generate_the_component_module() { let _: component_without_instructions::ComponentWithoutInstructions = @@ -63,3 +89,41 @@ fn generated_products_support_all_field_forms() { let _: core::marker::PhantomData> = core::marker::PhantomData; } + +#[test] +fn syntax_declarations_support_payloads_and_derived_value_patterns() { + use chumsky::Parser as _; + use vihaco::{InstructionSet, Parse}; + + let quoted = syntax_component::syntax::SyntaxValue::parser() + .parse("\"hello\"") + .into_result() + .unwrap(); + assert_eq!( + quoted, + syntax_component::syntax::SyntaxValue::Quoted(QuotedString("hello".to_owned(),)) + ); + + let bare = syntax_component::syntax::SyntaxValue::parser() + .parse("token") + .into_result() + .unwrap(); + assert_eq!( + bare, + syntax_component::syntax::SyntaxValue::Bare(BareToken("token".to_owned(),)) + ); + + let instruction = syntax_component::syntax::Instruction::parser() + .parse("runtime token") + .into_result() + .unwrap(); + assert_eq!( + instruction, + syntax_component::syntax::Instruction::Runtime( + syntax_component::syntax::SyntaxValue::Bare(BareToken("token".to_owned())), + ) + ); + + let _: ::Type = + syntax_component::syntax::SyntaxType::U32; +} From 0297ff3d767fb902f8e9b57ed79b713c2374e942 Mon Sep 17 00:00:00 2001 From: Rob Patterson Date: Wed, 12 Aug 2026 11:52:58 -0400 Subject: [PATCH 15/15] Updated CPU crate to reflect how components should be written with the vihaco rewrite; moved `component!`'s `instruction` block into a `runtime` block; updated docs --- .gitignore | 3 +- README.md | 11 +- crates/vihaco-cpu/src/component.rs | 1570 +++++++++++------ crates/vihaco-cpu/src/data.rs | 16 +- crates/vihaco-cpu/src/display.rs | 199 ++- crates/vihaco-cpu/src/instruction.rs | 474 ++--- crates/vihaco-cpu/src/lib.rs | 37 +- crates/vihaco-runtime-derive/src/component.rs | 219 ++- crates/vihaco-runtime-derive/src/lib.rs | 15 +- crates/vihaco/tests/component_macro.rs | 97 +- crates/vihaco/tests/multi_route_composite.rs | 24 +- demos/examples/counter-machine.rs | 2 +- demos/examples/counter-machine/src/machine.rs | 6 +- demos/examples/demo-vihaco-concepts.md | 8 +- demos/examples/demo.md | 4 + demos/examples/demo/src/cpu.rs | 30 +- demos/examples/demo/src/driver.rs | 6 +- demos/examples/demo/src/machine.rs | 12 +- demos/examples/demo/stdlib/arithmetic.rs | 18 +- demos/examples/demo/stdlib/channel.rs | 14 +- demos/examples/demo/stdlib/counter.rs | 20 +- demos/examples/demo/stdlib/stack.rs | 8 +- docs/examples/counter.rs | 19 +- docs/examples/quickstart.rs | 23 +- docs/examples/quickstart_parse.rs | 5 +- docs/src/pages/guide/components.md | 143 +- docs/src/pages/guide/composites.md | 20 +- docs/src/pages/guide/instructions-advanced.md | 182 +- docs/src/pages/guide/instructions.md | 158 +- docs/src/pages/guide/messages.md | 2 +- docs/src/pages/guide/parser-advanced.md | 24 +- docs/src/pages/index.astro | 16 +- docs/src/pages/quickstart.astro | 35 +- .../composite-surface-runtime-declaration.md | 25 +- vision/composite-syntax-runtime-plan.md | 11 +- vision/execution-pipeline.md | 9 +- vision/macro-generation.md | 15 +- vision/vihaco-cpu-rewrite-plan.md | 364 ++++ vision/vihaco-cpu-runtime-block-plan.md | 199 +++ 39 files changed, 2596 insertions(+), 1447 deletions(-) create mode 100644 vision/vihaco-cpu-rewrite-plan.md create mode 100644 vision/vihaco-cpu-runtime-block-plan.md diff --git a/.gitignore b/.gitignore index f5e5cd6d..0f4d9a26 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ CLAUDE.md .claude/skills/agents-update # docs site -node_modules \ No newline at end of file +node_modules +.pnpm-store \ No newline at end of file diff --git a/README.md b/README.md index b9d0bc24..ba92c34c 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,19 @@ use vihaco::{component, Effects, Execute, Execution, StepResult}; component! { component Counter { value: i64, } - instruction { Add(i64), Read, } + runtime { + instruction { Add(i64), Read, } + } } -impl Execute for counter::Counter { +// `component!` generates the component and these instruction structs. +// A containing `composite!` owns the machine-local instruction sum. + +impl Execute for counter::Counter { type Message = (); type Effect = (); type Fault = eyre::Report; - fn execute(&mut self, instruction: &counter::instruction::Add, _: ()) -> Result> { + fn execute(&mut self, instruction: &counter::runtime::instruction::Add, _: ()) -> Result> { self.value += instruction.0; Ok(StepResult { effects: Effects::none(), execution: Execution::Complete }) } diff --git a/crates/vihaco-cpu/src/component.rs b/crates/vihaco-cpu/src/component.rs index b640127d..23aaab47 100644 --- a/crates/vihaco-cpu/src/component.rs +++ b/crates/vihaco-cpu/src/component.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: MIT use eyre::Result; -use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Rem, Shl, Shr, Sub}; +use std::ops::{ + Add as _, BitAnd as _, BitOr as _, BitXor as _, Div as _, Mul as _, Rem as _, Shl as _, + Shr as _, Sub as _, +}; use crate::StepOutcome; use crate::data::CPU; -use crate::instruction::RuntimeInstruction; -use vihaco::Effects; use vihaco::program::{Type, Value}; -use vihaco::{Execute, Execution, StepResult, frame::Frame, traits::*}; +use vihaco::{Execute, NoEffect, NoMessage, StepResult, Supply, frame::Frame, traits::*}; + +pub use crate::instruction::cpu::runtime::instruction::*; impl Reset for CPU { fn reset(&mut self) { @@ -23,177 +26,211 @@ impl Reset for CPU { } } -impl CPU { - pub fn execute_instruction(&mut self, inst: RuntimeInstruction) -> eyre::Result { - self.clear_pending_pc(); - use RuntimeInstruction::*; - match inst { - Span(file, start, end) => self.op_span(file, start, end), - Label | FunctionStart | FunctionEnd => Ok(StepOutcome::Continue), - Breakpoint => Ok(StepOutcome::Breakpoint), - Branch(target) => self.op_branch(target), - ConditionalBranch(true_target, false_target) => { - self.op_conditional_branch(true_target, false_target) - } - Return(keep) => self.op_return(keep), - Call(arity, target) => self.op_call(arity, target), - IndirectCall => self.op_indirect_call(), - Halt => Ok(StepOutcome::Halt), - Print => Err(eyre::eyre!( - "Print must be handled via execute with CPUMessage::Print" - )), - Load(ty, addr) => self.op_load(ty, addr), - Store(ty, addr) => self.op_store(ty, addr), - Dup => self.op_dup(), - HeapAlloc(n_elements) => self.op_heap_alloc(n_elements), - GetItem => self.op_get_item(), - HeapDealloc => self.op_heap_dealloc(), - Const(v) => self.op_const(v), - Add(ty) => self.op_add(ty), - Sub(ty) => self.op_sub(ty), - Mul(ty) => self.op_mul(ty), - Div(ty) => self.op_div(ty), - Rem(ty) => self.op_rem(ty), - Neg(ty) => self.op_neg(ty), - Shl(ty) => self.op_shl(ty), - Shr(ty) => self.op_shr(ty), - Rol(ty) => self.op_rol(ty), - Ror(ty) => self.op_ror(ty), - BitAnd(ty) => self.op_bitand(ty), - BitOr(ty) => self.op_bitor(ty), - BitXor(ty) => self.op_bitxor(ty), - Not => self.op_not(), - And => self.op_and(), - Or => self.op_or(), - Xor => self.op_xor(), - Eq(ty) => self.op_eq(ty), - Ne(ty) => self.op_ne(ty), - Lt(ty) => self.op_lt(ty), - Gt(ty) => self.op_gt(ty), - Le(ty) => self.op_le(ty), - Ge(ty) => self.op_ge(ty), - } +pub mod message { + #[derive(Debug, Clone, PartialEq)] + pub struct FunctionInfo { + pub arity: u32, + pub start_address: u32, } + + #[derive(Debug, Clone, PartialEq)] + pub struct Print(pub String); } +impl vihaco::Message for message::FunctionInfo {} +impl vihaco::Message for message::Print {} + #[derive(Debug, Clone, PartialEq)] -pub enum CPUMessage { - None, - FunctionInfo { arity: u32, start_address: u32 }, - Print(String), +pub struct PrintEffect(pub String); + +impl Supply for CPU { + type Fault = eyre::Report; + + fn supply(&mut self) -> Result { + let start_address: u32 = self.stack_pop()?.try_into()?; + let arity: u32 = self.stack_pop()?.try_into()?; + Ok(message::FunctionInfo { + arity, + start_address, + }) + } +} + +impl Execute for CPU { + type Message = NoMessage; + type Effect = NoEffect; + type Fault = eyre::Report; + + fn execute( + &mut self, + instruction: &Span, + _message: Self::Message, + ) -> eyre::Result> { + self.span = (instruction.0, instruction.1, instruction.2); + vihaco::complete!() + } } -impl vihaco::Message for CPUMessage {} +impl Execute