From c21383ec1b06fa9b67c0e2e7f4cef356078da98c Mon Sep 17 00:00:00 2001 From: SavioCodes Date: Wed, 22 Apr 2026 18:31:55 -0300 Subject: [PATCH] docs: specify ownership-first reactivity model --- README.md | 1 + docs/first-slynx-file.md | 1 + docs/landing-content-inventory.md | 29 +- docs/language-surface.md | 1 + docs/reactivity-model.md | 312 ++++++++++++++++++++ middleend/README.md | 10 +- middleend/docs/reactive-graph-generation.md | 17 ++ 7 files changed, 368 insertions(+), 3 deletions(-) create mode 100644 docs/reactivity-model.md diff --git a/README.md b/README.md index 472d95ee..b08c7314 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Core project documents: - [CHANGELOG.md](CHANGELOG.md): repository-level changelog - [docs/language-surface.md](docs/language-surface.md): grounded overview of the current language syntax and constructs - [docs/first-slynx-file.md](docs/first-slynx-file.md): short tutorial-style example for new contributors +- [docs/reactivity-model.md](docs/reactivity-model.md): ownership-first reactivity design direction for components, style, and events - [middleend/README.md](middleend/README.md): IR design/specification reference - [docs/issue-reporting.md](docs/issue-reporting.md): guide for opening clear, actionable issues - [docs/landing-content-inventory.md](docs/landing-content-inventory.md): grounded inventory of what can already be published on the landing page diff --git a/docs/first-slynx-file.md b/docs/first-slynx-file.md index e6836e52..26821fb6 100644 --- a/docs/first-slynx-file.md +++ b/docs/first-slynx-file.md @@ -193,5 +193,6 @@ It does **not** document as finished: ## Where To Go Next - [docs/language-surface.md](language-surface.md) +- [docs/reactivity-model.md](reactivity-model.md) - [README.md](../README.md) - [middleend/README.md](../middleend/README.md) diff --git a/docs/landing-content-inventory.md b/docs/landing-content-inventory.md index c886c476..c1749b83 100644 --- a/docs/landing-content-inventory.md +++ b/docs/landing-content-inventory.md @@ -31,6 +31,7 @@ These files are the main factual references for landing-page content: - [RELEASING.md](../RELEASING.md): release/tag process - [CHANGELOG.md](../CHANGELOG.md): release history - [middleend/README.md](../middleend/README.md): middleend and IR design reference +- [docs/reactivity-model.md](reactivity-model.md): ownership-first reactivity model spec - [middleend/docs/reactive-graph-generation.md](../middleend/docs/reactive-graph-generation.md): reactive graph generation spec - [docs/component-slots.md](component-slots.md): slot model spec for the first implementation @@ -197,7 +198,30 @@ Do not present as fully implemented: - the full IR surface described in `middleend/README.md` - every operation/example in that document as if it already exists in the codebase -### 2. Reactive Graph Generation +### 2. Reactivity Ownership Model + +Status: `Spec / Design` + +Safe framing: + +- the repository contains a design proposal for ownership-first component + reactivity; +- it separates owned state, bound inputs, upward events/commands, style + derivation, and animation concerns; +- it is intended to guide frontend, graph, and IR work before the runtime model + is finalized. + +Evidence: + +- [docs/reactivity-model.md](reactivity-model.md) + +Do not present as implemented: + +- finished component reactivity on `main`; +- finalized event syntax; +- finalized style/animation language features. + +### 3. Reactive Graph Generation Status: `Spec / Design` @@ -215,7 +239,7 @@ Do not present as implemented: - reactive graph lowering as a finished compiler pass - final graph-driven IR generation on `main` -### 3. Component Slots +### 4. Component Slots Status: `Spec / Design` @@ -316,6 +340,7 @@ If the team wants to move quickly without inventing content, this order is the s 3. add a small API-reference page for the current root helpers 4. add a clearly labeled "Design Docs" section for: - [middleend/README.md](../middleend/README.md) + - [docs/reactivity-model.md](reactivity-model.md) - [middleend/docs/reactive-graph-generation.md](../middleend/docs/reactive-graph-generation.md) - [docs/component-slots.md](component-slots.md) 5. leave unfinished or speculative features out of the marketing copy until they land on `main` diff --git a/docs/language-surface.md b/docs/language-surface.md index 35490543..7dbc98a6 100644 --- a/docs/language-surface.md +++ b/docs/language-surface.md @@ -23,6 +23,7 @@ It does **not** try to document: For design-only topics, see: +- [docs/reactivity-model.md](reactivity-model.md) - [docs/component-slots.md](component-slots.md) - [middleend/docs/reactive-graph-generation.md](../middleend/docs/reactive-graph-generation.md) - [middleend/docs/linerize-the-graph.md](../middleend/docs/linerize-the-graph.md) diff --git a/docs/reactivity-model.md b/docs/reactivity-model.md new file mode 100644 index 00000000..916d24a4 --- /dev/null +++ b/docs/reactivity-model.md @@ -0,0 +1,312 @@ +# Component Reactivity Model + +This document captures the current design direction for component reactivity in +Slynx. + +It exists to close the semantic contract of ownership, binding, and upward +updates **before** parser, HIR, graph generation, and IR lowering start +depending on assumptions that may later need to be undone. + +## Status + +This is a design/specification document. + +It does **not** mean reactivity is already fully implemented on the current +`main` branch. + +This document is scoped to the **first reactivity implementation target**, +currently understood as the ownership model intended for `v0.0.1`-level work. + +The goal of this document is to record: + +- what direction is already recommended for the first implementation; +- what rules keep the model predictable for contributors and backends; +- what still needs explicit approval before syntax and lowering are locked. + +## Goal + +The reactivity model should make it obvious: + +- who owns a mutable value; +- which values are only inputs; +- how child components can request updates they do not own; +- where style and animation fit without becoming hidden mutation systems. + +The main objective is predictability. The first implementation should avoid +reactive behavior that looks convenient at the syntax level but becomes unclear +once lowering, debugging, and backend execution are involved. + +## High-Level Direction + +The recommended direction for the first implementation is: + +- every mutable reactive value has **one owning component**; +- reactive inputs flow **from parent to child**; +- a child may **read** a bound input, but should not directly mutate it; +- if a child wants to affect non-owned data, it should emit an + **event/command** upward; +- style should be modeled as **derived data**, not as a second hidden state + system; +- animation should be modeled as an **effect/transition layer**, not as just + another data-binding edge. + +This intentionally prefers explicit ownership over compile-time aliasing tricks. + +## Core Concepts + +### 1. Owned State + +An owned value belongs to one component and is mutated only by that component. + +Examples: + +- internal state declared and updated inside a component; +- public component data that is still owned by that component once the instance + exists. + +The important part is semantic ownership, not whether the syntax spells it as +`prop`, `state`, or something else in the future. + +### 2. Bound Input + +A bound input is a value that flows from an owner into another component. + +The receiving component may use that value in: + +- child props; +- computed expressions; +- style derivations; +- control flow; +- event payload construction. + +For the first implementation, the receiving component should treat that value as +**read-only**. + +### 3. Derived Value + +A derived value is any pure computation based on owned state and/or bound +inputs. + +Examples: + +- `count * 2` +- `isSelected && isEnabled` +- `ButtonStyle(primary, hovered)` + +Derived values are the natural input to the reactive dependency graph. + +### 4. Event / Command Output + +When a component wants to request a change to data it does not own, it should +emit an event or command upward. + +Conceptually: + +- child requests a change; +- owner receives the request; +- owner decides whether and how to mutate its own state. + +This keeps mutation localized to the owner and avoids hidden aliasing between +component boundaries. + +## Recommended Rules For The First Implementation (`v0.0.1` Scope) + +To keep the first implementation small and predictable, this document +recommends the following defaults: + +1. Every mutable value must have **exactly one owner**. +2. Parent-to-child reactive inputs should be treated as **readable**, not + directly writable, inside the child. +3. Child-to-parent updates should be represented as **events/commands**, not as + direct mutation of the parent's storage. +4. The first implementation should **not** rewrite child mutation of non-owned + data into direct parent mutation "behind the scenes" just because that is + possible at compile time. +5. Style should be expressed as a **derived result** of props/state, not as an + independent mutation channel with separate ownership rules. +6. Animation should be triggered by **state changes or events**, and should stay + conceptually separate from the pure value-dependency graph. + +These rules are meant to reduce ambiguity and keep later middleend/IR work +consistent with what contributors expect from the source language. + +## Preferred Direction vs Rejected Shortcut + +### Direction To Avoid For The First Version + +The first implementation should avoid a model where a child mutates a bound +value and the compiler silently rewrites that mutation into a write on the +parent's owned storage. + +Even if this can be optimized well, it creates problems for: + +- debugging ownership; +- understanding who is allowed to mutate what; +- reasoning about multiple children targeting the same source; +- layering style and animation semantics on top later. + +### Preferred Direction + +For the first implementation, the safer model is: + +1. owner data flows down; +2. child derives from it; +3. child emits requests up; +4. owner handles the request and mutates its own state. + +Conceptually: + +```slynx +component Parent { + pub prop count = 0; + + func handleChild(event: ChildEvent) { + switch(event) { + case .Increment(n): count += n + } + } + + Child { + count: count, + on_event: event -> handleChild(event), + } +} + +enum ChildEvent { + Increment(int) +} + +component Child { + pub bind count: int; + + Button { + on_click: _ -> emit Increment(1) + } +} +``` + +This example should be read as **semantic direction**, not as finalized syntax. + +## Style And State + +Style should not introduce a second hidden ownership model. + +For the first implementation, the safest rule is: + +- style values are computed from owned state and/or bound inputs; +- style evaluation can later reuse the same dependency-discovery machinery as + other derived values; +- style itself should not imply separate writable state unless the team + explicitly designs such a feature later. + +This keeps style understandable as "visual data derived from component data" +instead of "another reactive subsystem with its own mutation rules". + +## Animation And Effects + +Animation should be treated differently from plain value derivation. + +Why: + +- animation is often temporal; +- animation may need backend/runtime policies; +- animation may depend on transitions, not just current values. + +For the first implementation, a good default is: + +- state changes and events may trigger animations; +- animation lowering should be a separate effect/transition concern; +- the pure dependency graph should stay focused on values and deterministic + derived updates. + +This does **not** forbid future animation syntax. It only avoids forcing the +first reactivity graph to also become a complete animation scheduler. + +## Relationship To Graph Generation And IR + +This ownership model should guide the later middleend work: + +- the reactive graph should model **derived data flow** and **downward + propagation**; +- upward requests should remain explicit as **events/commands**; +- `@bind`-like operations should represent value propagation, not hidden writes + into someone else's owned state; +- `@emit`-like operations are the natural place to represent outward update + requests; +- `@rerender` remains a consequence of owner-visible state changes, not proof + that ownership boundaries disappeared. + +In other words: graph generation should stay about dependencies, while events +carry cross-boundary mutation requests. + +## What This Document Deliberately Does Not Lock Yet + +Some parts still need a final decision and should stay open until the team wants +to implement them: + +### 1. Final Surface Syntax + +This document does **not** finalize whether the language should spell concepts +as: + +- `prop` +- `state` +- `bind` +- `emit` +- `command` +- `event` + +It only locks the semantic separation they should represent. + +### 2. Event Handler Placement + +This document does not finalize whether handlers should always live: + +- inside the component body; +- in an extension; +- in a separate helper construct. + +### 3. Runtime Transport Strategy + +This document does not require a queue, closure, callback object, or any other +specific runtime implementation strategy. + +That remains a backend/runtime concern as long as the semantic contract stays +the same. + +### 4. Final Style Syntax + +This document does not lock the final syntax sugar for styles, style shorthands, +or style composition. + +### 5. Final Animation Syntax + +This document does not lock an animation DSL, transition syntax, or backend +policy for interruption, replay, or timing. + +## Non-Goals For The First Version (`v0.0.1` Scope) + +The first ownership-focused reactivity implementation does **not** need to +solve: + +- backend scheduling policy; +- async delivery guarantees for events; +- batching/coalescing strategy; +- animation engine semantics; +- finalized style DSL; +- cross-component mutation shortcuts that hide ownership. + +Those can be layered later once the ownership contract is stable. + +## Suggested Implementation Order + +The safest order is: + +1. finalize the ownership and event contract; +2. define frontend/type-checking rules for owned vs bound values; +3. generate the dependency graph only for derived/downward updates; +4. lower upward requests through explicit event/command semantics; +5. only then design style/animation syntax on top of that base. + +This keeps the first reactivity work small, predictable, and easier to carry +forward into HIR, graph generation, and IR. diff --git a/middleend/README.md b/middleend/README.md index 71ac366d..84b1a2cc 100644 --- a/middleend/README.md +++ b/middleend/README.md @@ -18,6 +18,9 @@ What is true today: The long-term goal is an SSA-oriented, strongly typed IR that tells a downstream compiler what to do without forcing a single runtime strategy. +For the ownership-first component reactivity direction that should guide graph +generation and event lowering, see [../docs/reactivity-model.md](../docs/reactivity-model.md). + For the stage that extracts reactive dependencies before linearization/IR lowering, see [docs/reactive-graph-generation.md](docs/reactive-graph-generation.md). @@ -317,7 +320,7 @@ Differently of default values, special values are primitives that are expected t ### UI Operations Anything on the IR that initializes with '@' and is being used as an instruction, is an specific UI Operation, which determine what the UI itself should do. If being used as a value, then it's the visual reference to a handle of some internal string -On Components, @binds are way to determine which value on the component should update which dependency. On the %Counter example above, we had +On Components, @binds are a way to determine which value on the component should update which dependency. On the %Counter example above, we had ``` @bind %count -> field #t0, 0; @@ -327,6 +330,11 @@ On Components, @binds are way to determine which value on the component should u which means that, on %count update, it updates with the new value, the value of the field 0 of #t0. For the field 0 of #t1, it updates it using `%count |> f`, which means that the value of `call f, %count`, is used as the new value. The `@emit p0, %count` on the function, tells that `p0` should execute its `%count` binds. And after executing them, send a re-render with @rerender. +For the first ownership-focused reactivity model, `@bind` should be read as +downward/derived value propagation, not as hidden mutation of data owned by +another component boundary. Cross-boundary update requests should stay explicit +through event-like operations. + * @bind: which follows `@bind %property |> func -> value`, means that on update of `%property` inside the component we are defining, updates the provided `value` * @emit: which follows `@emit Component, %property`, means that it should execute the binds related to `%property` of the provided `Component` * @rerender: which follows: `@rerender Component`, means that the `Component` should be re-rendered diff --git a/middleend/docs/reactive-graph-generation.md b/middleend/docs/reactive-graph-generation.md index 30b13fd8..3ff9b67d 100644 --- a/middleend/docs/reactive-graph-generation.md +++ b/middleend/docs/reactive-graph-generation.md @@ -4,6 +4,11 @@ This document specifies the stage that builds Slynx's reactive dependency graph for a component. It intentionally stops at graph generation. Linearization and IR lowering are left as separate phases. +This document assumes the ownership-first semantics described in +[`../../docs/reactivity-model.md`](../../docs/reactivity-model.md). +In particular, this stage is meant to model derived value dependencies and +downward propagation, not hidden cross-component mutation. + ## Goal The graph generation stage extracts reactive dependencies from a typed component @@ -17,6 +22,9 @@ The graph itself should stay generic enough to be reused by future compiler features and tooling. The IR should only consume the graph output; it should not own the dependency discovery logic. +For the first implementation, upward event/command requests should stay explicit +instead of being encoded as if they were ordinary data-dependency edges. + ## Placement In The Pipeline For `v0.0.1`, the intended order is: @@ -53,6 +61,9 @@ The stage needs: - the typed expressions used by child properties and reactive updates; - the field index/type metadata for child components and specialized nodes. +It should not require backend-specific event transport details in order to +build the value-dependency graph. + ## Outputs The output of this stage is a graph definition for one component. @@ -192,11 +203,17 @@ Each independently triggerable update should become its own edge. If two child fields both depend on `count`, they become two edges. If one target depends on `count |> f`, that transformation belongs to that edge. +For the first ownership-focused model, this means value updates and derivations. +It does not mean "any possible cross-component reaction" should become a graph +edge. + ### 4. No Lowering During Generation This stage should not emit IR instructions and should not decide label/block layout. It only builds the dependency graph. +Event/command transport for upward requests remains a separate concern. + ### 5. Cycle Detection Is Mandatory The generated graph must be validated as acyclic before linearization.