Skip to content

FE-1835: Add a compiler from Petri nets to Zeroth reactive modules, with its playground - #167

Draft
kube wants to merge 14 commits into
mainfrom
cf/fe-1835-zeroth-compiler-playground
Draft

kube wants to merge 14 commits into
mainfrom
cf/fe-1835-zeroth-compiler-playground

Conversation

@kube

@kube kube commented Oct 2, 2026 •

Copy link
Copy Markdown

Important

Experimental
New proof of concept in pocs/, a private package.

Summary

Adds pocs/zeroth-playground: a compiler from Petri nets, written in YAML as the Petri net IR, to Zeroth reactive modules written with zrth.sugar. A playground shows each net beside the Python it compiles to, so HASH and Zeroth can read, run and extend the compiler in one public place.

The project has four folders. compiler/ stands alone on js-yaml and zod, and is laid out so a new option, emitted construct or diagnostic touches few files. examples/ climbs a ladder from one step to clock races, semantics/ holds the questions open between HASH and Zeroth on what a net means as modules, and playground/ builds to one HTML file with a view for each. pnpm check:zrth imports every example's Python into zrth's spn branch, and every listed example constructs.

zeroth-playground.mp4

Links

Changes

Compiler

  • One zod schema defines the IR

    The type is inferred from it.
    Parsing reports schema and reference errors at their YAML line.

  • One table holds the options

    It derives the option types and says why an option does not apply to a net.
    An option that does not apply is dropped with a warning.

  • Each lowering states what it cannot express before it runs

    Monolithic and modular shapes write coins over LIA or LRA, and clock rates write SPN.

  • Emitters write each Python line with the net item it comes from

    The Python trace is right by construction, and hovering uses it.

  • Code parser is an injectable hook

    Without one, a net whose guards, rates or kernels are code is refused with code-not-parsed.

  • Clocked output follows zrth's spn branch

    The partial if-then is ite(cond, x, None).
    A variable that does not move has the flow 0.

  • compiler/mapping.md is the design shared by HASH and Zeroth

    Each mapping, its options, and the open questions on the semantics.

Examples

  • Examples climb from one step to clock races

    Steps cover one step, conflicts, capacities and arc kinds.
    Rates cover coins, clocks, two inputs and conflicts under clocks.

  • Each page opens on the problem it shows

    Open questions are limited to the compilation's semantics, each on one page.

  • Each example keeps its net, page, options and figures in one folder

Semantics

  • One folder per open question, with a typed meta.ts and a page

    Topic, owner, status, and the examples that show it with the options and the net item.
    Example pages and compiler/mapping.md reference a question rather than restate it.

  • Semantics view lists the questions by topic and quotes the lines that show each

    It compiles the linked example at the linked options and quotes the item's lines.
    "Open in Playground" opens that example with those options.

Playground

  • Playground builds to one self-contained HTML file

    Three views: Playground with the IR editor, the Python and the net preview, Compiler with the pipeline's stages, and Semantics.

  • Hovering a line or a node lights its counterparts in the other views
  • URL hash routes the app, and every link in it is a plain <a href>

    #capacity?shape=modular opens an example under given options, so a link can be shared.
    Back and Forward restore the view and the example.

  • Each component carries its own CSS over shared tokens and cascade layers

Tooling

  • Build fails on every React Compiler diagnostic
  • pnpm check:zrth checks the Python against a zrth checkout

    It takes the checkout path from ZRTH_PATH, and is not part of CI.

Known issues

  • Clock rates refuse capacities and arc weights above one

    SPN tests a count against zero and moves one token at a time.
    mapping.md holds the questions.

  • Playground passes no code parser

    A TypeScript parser does not fit the one-file budget, so nets with code strings are refused.

  • pnpm check:zrth checks that the Python constructs, not that it runs
  • Maths renderer's @xmldom/xmldom is lifted to 0.9.12 by an override scoped to speech-rule-engine

    Released rehype-mathjax ships MathJax 3, whose speech-rule-engine pins 0.9.10.
    Its MathJax 4 move is in open upstream PRs.

  • Monaco's dompurify is lifted to 3.4.16 by an override scoped to monaco-editor

    Monaco 0.57 pins 3.4.15, which has a low-severity advisory.

Test coverage

  • compiler/**/*.test.ts:

    Parsing with lines, option applicability, every refusal code, each lowering, emit-time traces and the code-parser hook over captured code trees.

  • compiler/lower/lower.test.ts:

    A differential test runs random nets against a reference step.

  • examples/examples.test.ts:

    Each example's outcome and its Python at its opening options.

  • semantics/register.test.ts:

    Every question's folder, examples, options and net item resolve, and nothing is orphaned.

  • playground/**/*.test.{ts,tsx}:

    Routes and their hashes round trip, option and module edits, the preview graph, the pipeline layout, the question evidence, and every page rendering.

  • pnpm check:zrth against zrth's spn branch:

    Every listed example constructs.

How to test

  • cd pocs/zeroth-playground && pnpm install && pnpm dev
  • Open http://localhost:5173
  • Hover Go: in the IR

    Expect its Python lines and the Go node lit

  • Compiler options > Shape > modular

    Expect one module per transition and per place

  • Example picker > Conflicts under clocks

    Expect SPN modules and two clocks racing in the page

  • Hover the Pool place in the preview

    Expect its IR and Python lines lit

  • Compiler

    Expect the pipeline graph with a card per stage

  • Semantics > Ties under clocks

    Expect the question, the Python it quotes, and an Open in Playground button

kube added 4 commits October 2, 2026 10:50
…layground

pocs/zeroth-playground holds three folders:

- compiler/ reads a Petri net written in YAML and writes the Python that
  builds it as Zeroth reactive modules with zrth.sugar. It imports only
  js-yaml and zod. Options choose the shape, how rates fire, how conflicts
  resolve, the marking theory, control, the time step, slots and the file
  layout. Every line of output is traced back to the item of the net it
  comes from. A code parser can be injected for nets whose guards, rates or
  kernels are code.
- examples/ holds one folder per example: the net, its page, its options
  and its figures.
- playground/ is a Vite and React app that builds to one HTML file: the
  net, the Python, a preview of the net, a page per example, and a view
  of the compiler's stages.

compiler/mapping.md is the design shared by HASH and Zeroth, with the open
questions on the semantics. pnpm check:zrth imports every example's Python
into a zrth checkout to check that it constructs.
The examples climb two rungs. Steps: one step as a module, who gets the
token, a capped place, and weighted, read and inhibitor arcs. Rates: rates
as coins, rates as clocks, two inputs under clocks, and conflicts under
clocks. Bucket stays unlisted, to show what a net with code strings looks
like when no code parser is given.

Fork merges into the conflict example, the coins page moves to the
birth-death net, a new arcs net takes over the arc lessons, and the queue,
SIR, boiler and drones examples go. Cycle opens in the monolithic shape, so
the one-class step comes first. Each open question lives on one page, and
examples/README.md lists the ladder with what each example teaches.
theme/ now holds three files: tokens.css (colours, type sizes, spacing,
radii, shadows and durations, and the layer order), base.css (the reset,
one focus ring, a zero-specificity button reset, the shared mono capitals
class and one reduced-motion block) and prose.css (the docs pages, the
guide and the stage card). Every component imports its own file, written
with native nesting. Monaco's CSS sits in a vendor layer through a small
Vite plugin, so our rules win without extra specificity; only the
important declarations that override Monaco's inline styles remain.

A pixel diff of the main views and states shows no change apart from
reduced motion, which now stops every transition, and the accent focus
ring on a few scroll areas that showed the browser's.
The Module graph card no longer claims the examples open modular, since
Cycle opens monolithic and the clock examples open under clocks; the
Options card already says what each opens with. The Lower card names
Bucket, the one example with code strings. mapping.md names zrth's main
branch without a local remote name.
@kube kube self-assigned this Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Dependency Review

The following issues were found:

  • ❌ 1 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses.
  • ⚠️ 31 packages with OpenSSF Scorecard issues.

View full job summary

… its parent

speech-rule-engine 4, reached through rehype-mathjax and mathjax-full 3,
pins @xmldom/xmldom at exactly 0.9.10, which has published advisories.
No released rehype-mathjax lifts the pin; its move to MathJax 4 is in open
upstream PRs. The override is scoped to speech-rule-engine and documented
in playground/README.md, to drop once upstream releases.
@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
petrinaut-gaios Error Error Oct 2, 2026 6:06pm UTC
petrinaut-hazel Ready Ready Preview Oct 2, 2026 6:06pm UTC

Request Review

kube added 3 commits October 2, 2026 12:22
…es that show it

The questions on what a net means as modules, and on what Zeroth can
express, lived twice: as amber boxes on the example pages and as bullets
under the mappings in compiler/mapping.md, kept in step by hand. They now
live once, in a fourth root folder, semantics/, with one folder per
question, and a third view lists them by topic.

- semantics/register.ts collects the folders as examples/catalog.ts does:
  six topics (steps, conflicts, places, rates, colours, theories), and per
  question a title, an owner, a status (open, proposed, settled) and where
  it shows: an example, the options that bring the behaviour out, and the
  net item whose emitted lines carry it. CompilerOptions and NetItem are
  the compiler's types, so a renamed option value fails tsc. intro.mdx is
  the view's opening text and README.md says how to add a question.
- Eighteen questions: the thirteen boxes of the pages, the four semantic
  ones mapping.md alone listed (immediate transitions, kernels and
  coloured tokens under SPN, an LRA flow that reads its own state), with
  "how an open SPN module is driven" and "how picks are driven under SPN"
  merged into one, and one settled entry, read and inhibitor arcs in the
  modular shape, to show the log works. Each page says the question, what
  the compiler writes today with the exact Python, and the options on the
  table; a settled one adds its decision and date.
- The Semantics view: the list by topic, with a status dot, the title and
  the owner, a listbox the arrow keys walk; the card with the page, then
  "Shows in", one block per showing that compiles the example off the
  catalog at those options and quotes the item's lines, up to 14, or the
  first diagnostic when the compile refuses. "Open in Playground" opens
  the example in the Examples view with those options. #semantics opens
  the view and #semantics/<id> a question. The App holds the selection
  and provides a NavigationContext, so a page's reference can open its
  question and a showing can open its example.
- The pages reference a question with <Question id="..." />, an amber
  card with the owner, the status and a way into the Semantics view; an
  unknown id throws and fails the page render test. OpenQuestion is gone.
  The guide's one box, on where a diagnostic lands, was about the
  playground's tooling, so it is a paragraph. mapping.md links each
  register question under its mapping and keeps the tooling questions
  alone; register.test.ts checks that every link resolves and that no
  question is orphaned.
- Shared pieces extracted where the second use appeared: ui/excerpt.ts
  and ui/code-excerpt.tsx hold the quoted-lines model and view the stage
  samples and the showings both use, with the samples' cap of 8 kept in
  stage-sample/excerpt.ts; ui/back-link.tsx the "‹ All stages" link the
  stage card and the question card share; ui/roving.ts the arrow-key step
  of the picker and the question list; setOptionLabels in
  option-labels.ts the "Shape modular, Time step 0.5" labels; .kicker
  moves to base.css. The Compiler view renders as before.
- The view switch gains a third segment; the anchored pill glides to it.
  pnpm screenshot adds semantics.png and semantics-question.png.
- Tooling questions stay in mapping.md alone, as the simplest option: the
  register is about what a net means.

Checked headlessly on the build: the intro and the grouped list, a
selected card with its quoted lines and showings, Open in Playground
switching the view with the options in the panel, Open in Semantics from
an example page, the back link and the keyboard, a refused showing, the
settled question, the pill mid-glide, and the Compiler view's stage card.
The first segment of the view switch reads Playground, and the view
behind it is playground/playground-view/. "Examples" named the picker's
list, which it keeps, and misnamed the workspace: it is where the IR, the
options and the module are edited, whatever example they started from.

- View id "examples" becomes "playground", ExamplesView PlaygroundView,
  and the folder and its two files move with git mv. The hash scheme is
  unchanged: #<example-id> opens the Playground view on that example.
- The comments, the stage texts, the Compiler guide and the READMEs that
  named the Examples view now name the Playground view.

Checked headlessly on the build: the three segments switch, each view
shows its own panels, the picker opens another example and its page
follows, and a question's Open in Playground lands in the Playground
view with its options.
…r in Semantics

The header is three columns with equal sides, so the switch sits at the
centre whatever the brand and the picker take. The title reads "Petri net
to reactive modules", in the header and the browser tab. The example
picker and Reset show only in the Playground and Compiler views, which
work on the picked example; the Semantics view is general. The root
README gains the Semantics view.
…ditor

Monaco 0.57 pins dompurify at exactly 3.4.15, which has a published
advisory. The override is scoped to monaco-editor and documented beside
the xmldom one in playground/README.md, to drop once a Monaco release
depends on 3.4.16 or later. The hover card, which dompurify sanitises,
still renders.
The header's controls share one height, radius and shadow through two
variables on the header: a squircle of half the height where the browser
draws one, 0.7rem otherwise. The picker trigger and the Reset button drop
their hairline border for the switch's lift, and hover tints their
background instead of darkening a border. Fonts are unchanged.
The hash is where the app is. useRoute reads it through a store that
follows hashchange, and the app renders the view it names and keeps the
document of the example it names. The view switch, the question list, a
page's question card, the Semantics card's back link and a showing's
"Open in Playground" are plain links to hashes; the picker navigates.
A hash can carry the options an example opens under, as a query the
options table checks, so a showing's link reproduces its behaviour and
such a link can be shared. The view switch returns each view to what it
last showed.

A hash naming another example, or other options, rebuilds the document;
edits in the panel and the editors do not write the hash, and Reset
returns to the document as the hash opened it. The navigation context
goes, since links replace it.
The app's grid had no column definition, so its one column grew to the
longest unbreakable line of the view shown, the quoted Python of a
Semantics question, and the panels spilled past the window when the tab
changed. The column is now minmax(0, 1fr), and the workspace may shrink,
so a long line scrolls inside its block instead.
A title stays on one line. One the row cuts fades into the background
over its last 2rem, through a mask an animation on the title's own scroll
timeline applies: the timeline is active only while the title overflows,
so a title that fits stays whole. A browser without scroll-driven
animations clips the title instead.

This branch had an error being deployed

2 failed and 1 active deployments
Preview – petrinaut-hazel — 2aef4544 Deployed Oct 2, 2026 by vercel[bot]
Preview – petrinaut-gaios — 2aef4544 Deployed Oct 2, 2026 by vercel[bot]
Preview – hcore — 2aef4544 Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant