Skip to content

Latest commit

 

History

444 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

monumynt

An experimental sandbox for a visual flow-based programming language. The language design lives in the documents under plans/; this repository implements the non-graphical building blocks — the data structures and the compile pipeline that the eventual visual editor would sit on top of. Layout, rendering, and the diagram-editor side are out of scope here.

The code is ReScript 12 compiled to ES modules and run on Node.

Design documents

The design work has run well ahead of the code. The code implements list/option iteration, case splits, join and filter, partial collects, registers (the Delay pair), the whole-table Cross, structs, stream flows (the pulled-on-demand axis), and a textual surface that round-trips; everything else in the design documents is design-only so far.

Start with plans/README.md — the index, with reading order and per-document status. The shortest path in:

  • plans/language-design-philosophy.md — the seven principles every new construct is evaluated against.
  • plans/core-model.md — the core model in current vocabulary: value vs flow wires, uncollect/collect, no time travel, binary join, bundles, barriers-not-bottlenecks, the flow kinds.

Rejected ideas are recorded in place, in the doc that owns the topic, each with the reason it must not be pursued again.

The rest, grouped in the index: the data-representation spec and the textual form; iteration state (the biggest open area — two live candidates, deliberately undecided); streams, async, and incremental flow kinds; the partial collect, Cross, time-travel completion, and transformation levels; validity checking without types; the compile strategy (implemented and planned); the five tough use cases; and the implementation sequencing.

What the code implements

The pipeline:

text  ─lex/parse/resolve─┐
                         ├─→  Program (node set + outputs)  ─→  JsAst  ─→  JavaScript source
handles (Build)  ────────┘        derive → complete → check → annotate → codegen
  • src/JsAst.res — typed AST for a useful subset of JavaScript.
  • src/JsPrint.res — precedence-aware pretty-printer.
  • src/JsBuild.res — smart constructors so building JsAst reads like the JS it produces.
  • src/Program.resthe program of record: the ports-first representation. A program is a node set plus distinguished outputs (no root expression). Node kinds are Lit, App, Uncollect (list / option / stream / case — the opener), Collect (the consumer), binary Join, Commute, Cross, the DelayRead/DelayWrite register pair, and Aggregate/Disaggregate (struct construction and field projection); ports are named value/flow refs, with per-alt ports on a case split (no Branch node). Sharing is opt-in via node identity: bind once in ReScript, reference twice.
  • src/Build.res — typed handles over Program (strings below, typed handles above); also collects the node set, closed by finish(~outputs).
  • The compile pipeline as pure passes (plans/compile-strategy-design.md): DeriveComplete (inserts a Cross for a sibling-opens combine) → Check (well-formedness witnesses returned as data) → AnnotateCodegen, wired by Pipeline.res. Codegen is a pure let-floating placer with a (node, port, context) memo; every node becomes a lazy binding and every reference forces, so runtime laziness handles "compute only when needed" and "compute only once". Rationale in plans/lazy-compile-design.md.
  • src/Text*.res — the textual surface: lexer, parser, resolver (into Build), and a total printer that round-trips.
  • src/Runtime.res — the emitted prelude, in layers: three lazy helpers for the eager fragment, plus the Delayed-cell stream runtime (iterative force with path compression, zipStream, listToStream) when a program uses a stream flow.
  • src/Main.res — the smoke suite (npm start): 348 checks that build programs from text and handles, compile them, eval the output, and compare against author-written expected values, plus text round-trips. Coverage spans the value fragment, sharing and placement, list/option/join flows (multi-collect, nested, flatten), case splits and filters, partial collects, registers, structs, the whole-table Cross (including one opened inside an enclosing loop), and stream flows.

src/ARCHITECTURE.md is the deep map: module status, the decisions taken, and the worklist.

Running

npm install         # one-time
npm run build       # rescript compile
npm start           # node lib/es6/src/Main.res.mjs — runs the test suite

npm run dev for watch mode.

Possible next steps

None committed to.

The design-vs-code gap is now mapped in code: architecture-stub modules under src/ (Stream, Async, Incremental, Cut, Fail, CollectFamily, Property, OrderDemand, Boundary, Effects, FocusedUpdate, Saturation, Edit) stage the types, adopted decisions, and settled rejections for everything in plans/ that has a worked representation but no implementation. See src/ARCHITECTURE.md, "Architecture stubs", for the index. The items below predate that map and remain live.

  • The poset round — the context-model generalisation (linear prefix → a genuine series-parallel poset) that the remaining Codegen.Todo gaps wait on: cross of non-list axes, and a running view over a product driving flow the reading collect does not itself scan. A filtered axis is now crossable — an axis may be a join chain (join(list, case-alt)) rather than a single open, which is the first of the three filtering regimes: filter an axis by its own element and the same rows survive for every point of the other, so it is "rectangular, just smaller". The table is built by walking each axis's chain, one row per kept firing; a consumer traverses by index over the axis's kept count, walked once and shared, so both orders still read the one table and the user's computation still runs once per point. Such an axis can be held as well as collected — the fibered read over a filtered (or flattened) axis — because holding an axis is having its coordinate, and a chain axis's coordinate is the running count of the firings it produces: the same order the table's rows were pushed in. The holding chain mints one counter and hands each firing const i = c++, which the traversal reads exactly where a plain axis's loop index would be. Filtering by the other axis's element is the third regime — no product exists, and the invariance rule witnesses it. A product opened inside an enclosing loop — the per-group cartesian product — now compiles, and completes: a product carries its own exterior, so its shared table is built once per point of that exterior and everything the top-level product had (both traversal orders on one table, the fibered register, the fibered traversal) comes along one layer in; and a sibling combine inside a loop is completed by crossing its span's sibling frontier, the loop's own axis dropped as shared context. Completion reaches a filtered axis too, when the author drew it: such a combine names both of the axis's layers in its span, and the drawn chain claims them as one axis. What completion will not do is manufacture the join — it inserts only operators whose value-level shadow is the identity, and a join changes firing structure, which is meaning the author must draw — so an undrawn filtered axis stays a witness whose remedy is the drawn chain. The transposing commute now compiles: over a crossed pair commute is transpose, and transpose re-reads a product rather than restructuring it, so a commute output port simply denotes its operand swapped and the transposed consumer is another permutation indexing the one shared table — nothing to emit, and the user's computation still runs once per point. (Commute over a genuine nesting is the other operation the one word names — the directed sequence, with its short-circuit — and it now compiles too, over a stream: see Streams below.) Fibered products (a product collected over some of its axes while enclosing loops hold the rest) now compile at any fiber width, on a flattened path read as a set of axes — the poset-valued context report, which would put that order-freedom in the type rather than in a convention, is still ahead. Poset.res has the algebra; src/ARCHITECTURE.md worklist item 8 is the map.
  • The partial collect's cell-set lattice. DONE. The design's HTTP program compiles end to end: a partial collect merges two of four cells, one handler runs at the merged context (the logAndFallback step, admitted by the containment theorem {A} ⊆ {A, B} stated as a step-availability relation), and a covering collect reconverges over disjoint cell sets — two singletons and a pair. Merges nest; a merged value can be read from inside one of its cells by a chain that opened that cell some other way; a merged flow can sit anywhere in a join chain, not just at its end; and values on overlapping cell sets combine at their meet ({A,B} and {B,C} at {B}) — subject to the same discipline a product combine gets: the meet must be a set the program constructed. The two cell-set items that lived on other rows — a register over a partial merged driving flow, and its running view — have since landed too: both were the same walk one width out, so both arrived by assembling through the cell chain's own recursive level walk rather than by growing machinery of their own. A partial level is a dispatch that keeps k cells, and every walk that handles one cell handles k by recursing.
  • Aggregate/Disaggregate for struct construction and field projection. DONE (as pure value nodes — Aggregate builds an object literal, Disaggregate projects one value port per field; both compile like an App, let-floated to where the fields jointly live). The textual surface has since landed too: x, y -> aggregate name, age => p mirrors an application (the chain's sources, in order, are the field values), and p -> disaggregate name, age => d mirrors split (a multi-output node named once, its fields reached as d.name). Both print and round-trip, which makes the printer total for the first time — its last failwith was the struct case. Still owed on the literal side: an object-literal leaf term, so a struct-valued source need not go through the js "..." escape hatch.
  • Diagrams as the top-level structure — the spec's Diagram type, compiling to a JS function per diagram. Now has a forcing argument beyond spec fidelity: a Delay write half can be root-unreachable in a complete program, so the program of record is a node set, not a root expression (plans/iteration-with-state-design.md, "What it forces to the surface: the program is a node set").
  • Streams — started, and the prediction held: a new species in Annotate, cells in Runtime.res, and an emitter, with no restructuring anywhere. A stream flow opens a value into a per-element flow pulled on demand — each element computed only when a downstream consumer asks, and then only once — and it is spelled open stream beside open list, structurally identical down to the ports, the order class, and the handle. The runtime is the prototype's Delayed cell made synchronous, which is what creates the stack hazard the design names, so both of its hard requirements live in the primitive: force follows redirect chains iteratively and path-compresses as it goes (200k consecutive skips run without overflowing). The emitter is the eager list emitter with the loop taken out — same placement, same memo, same element pre-memoisation — so loop-invariant hoisting became pull-invariant hoisting with nothing written for it, and a stream flow nested inside an eager list flow needed nothing at all. Multi- output works on the baseline by construction, which is why the consumer-set bookkeeping is an optimisation pass and not a prerequisite. The sequence commute — the motivating operation, stream<option<X>> to option<stream<X>>, short-circuiting at the first absence — now compiles as well, and it cost no placement work at all, because commute is per-collect output construction and nothing else: the emitter is the stream collect with the option's guard as one more level. Its fold uses two of the runtime's three moves and pointedly not the third — it never emits a cons, since one answer about the whole stream cannot be handed out a cell at a time, so each firing becomes the rest (accumulating on the side) and an absent option abandons the rest without ever forcing the tail. That is also what keeps the walk a redirect chain the iterative force loop follows rather than a per-element recursion. A register over a stream compiles too — the check half was already done (a stream's order is owned: the source's, delivered on demand), and the emitter is the register's fold in the sequence commute's shape, the accumulator advancing on the side while each firing becomes the rest, so forcing final walks the whole source (what a fold means) without costing a stack frame per element. Ahead: the stacked commute stages, the stream flatten, the running view over a stream (a scan can hand out cells as it goes, so its fold cons-es — a different shape), and Shape C proper, one memoised cell per node, which is what buys back cross-consumer sharing.
  • Async, incremental — each a new species in Annotate
    • cells in Runtime.res + an emitter, per plans/implementation-strategy.md.
  • Structural tests — golden-file the generated JS.

Layout

plans/                               design docs — see plans/README.md
src/
  ARCHITECTURE.md                    the compiler map — read this first
  JsAst.res                          typed JS AST
  JsPrint.res                        precedence-aware printer
  JsBuild.res                        smart constructors
  Program.res                        the program of record (ports-first node set)
  Build.res                          typed handles over Program
  Context.res  Poset.res             flow-context (linear path + SP poset)
  Derive.res  Complete.res  Check.res  Annotate.res  Codegen.res   the pipeline passes
  Pipeline.res                       pass orchestration
  Runtime.res                        the emitted prelude (eager + stream layers)
  TextLex.res  TextParse.res  TextAst.res  TextResolve.res  TextPrint.res   the textual surface
  Main.res                           the smoke suite + examples
rescript.json                        ESM output, lib/es6/, .res.mjs suffix
package.json                         "type": "module"

lib/ is gitignored; only .res sources are tracked.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages