aboutsummaryrefslogtreecommitdiffstats
path: root/docs/design
Commit message (Collapse)AuthorAgeFilesLines
* Check pure-serve in Pure, not in aupeunhhu2026-09-272-1/+6
| | | | | | | | | | The HTTP host's end-to-end checks (documents against the earlier server, lending, kill -9 durability, JSON, timers, links, webhooks and answers) sat in aup's tests, so a change to the host was verified from the toolchain. They now live in tests/check.py; make check builds their apps with the sibling aup and runs them after the unit tests, which stay beside their parts. Design 009 states the rule for all three repositories.
* Show the new layout costs nothing, and keep code where it waseunhhu2026-09-271-0/+9
| | | | | | | | | | | | The benchmark runs an earlier pure-serve build in turn with the current one (--baseline). Against the build before the move, linking through libpure.a costs nothing: a one-command build with the library's object order has the same size and the same CPU per request. What moved was code placement, so the runtime links last again, as it did; the two orders now differ by less than rounds of one build do (total -0.4%, page +1.2%, mixed -0.1%). The binary is 5,008 B smaller: unused flow.c functions stay in the library, and the linker's A53 erratum padding is gone.
* Lay Pure out as a core with the parts selected around iteunhhu2026-09-272-2/+62
| | | | | | | | | | | | | | | | | | | | | The app model, the tree store and the HTTP host lived in prototypes/connections among the earlier document runtime, the store was known only by its format's name, and Vega compiled six Pure sources by path. Now, as the README's principle has it: core/ runtime, flow and the app model (module*.c), with what the core asks of storage (state.h, store.h) store/ the tree store (PURETRE3, PUREREC1) serve/ pure-serve: HTTP host, outbound delivery, JSON in place cli/ pure-run, pure-edit-module A top-level make builds libpure.a (app model and store, for hosts to link) and the hosts; make check runs the store's and the app model's tests. The earlier runtime still builds from prototypes/connections against core/ and store/ (check, check-tree and check-core pass). Design 009 records the rule: parts by role behind libraries; git repositories split by coupling, so the store stays here while its contract moves with the core, and Vega stays apart.
* Make an API's answers the app's events (--reply OUTPUT=INPUT)eunhhu2026-09-271-1/+3
| | | | | | | | | | | | | | | | | | | | | The final answer to each send of a wired output (a 2xx, or a 4xx but 408 and 429) becomes a message event of the given input: fields from the answer's JSON object by name, other members skipped; a field the answer lacks from the send's field of that name and type, so the request's own id is there; a u32 http_status field is the status. It is applied with the send's number from the wire's source, before the send is trimmed and in the same save, so an answer that comes again after a restart (the API repeating itself for the Idempotency-Key) applies once. Sends that wait for answers go one per request; answers are saved with the next round, or at once when none waits. A server that writes an answer in pieces holds the second until the first is acknowledged, and a delayed acknowledgement stalled every answer 40 ms: the client now acknowledges at once (TCP_QUICKACK) after each read. Against a payments API that answers 200 or 402, with the sender killed (-9) midway, paid and declined orders matched the API's answers one to one.
* Deliver to https receivers, verifying their certificates (083)eunhhu2026-09-271-1/+2
| | | | | | | | | | A wire to an https:// URL speaks TLS 1.2 or later through OpenSSL: the receiver's certificate must chain to the system's CAs (or to the file --send-ca names) and name its host (SNI and SSL_set1_host, or the IP address). An untrusted receiver gets nothing: the send stays in the outbox, the wire logs it and tries again every 10 s. pure-serve now links libssl and libcrypto.
* Deliver an app's saved sends over HTTP, in order and in batches (083)eunhhu2026-09-271-0/+6
| | | | | | | | | | | | | | | | | | | | pure-serve --send OUTPUT=http://host:port/path wires an output: after every save the owner queues the new sends as JSON (fields and groups) and a thread per output posts them in order with X-Pure-Source, X-Pure-Sequence and Idempotency-Key. A 2xx, or a Pure receiver's refusal, delivers; the owner then trims. 408, 429, 5xx and no answer are sent again after a pause growing to 10 s, other answers every 10 s: nothing is dropped. A Pure receiver applies a delivered message with pure_app_deliver (now taking placed fields) and advertises X-Pure-Batch: 256; the sender then posts up to that many numbers in a row as one JSON array (X-Pure-Count), applied in order in one burst with one save. Delivery kept up with orders once batched (391 deliveries/s one at a time; a backlog of 20,000 went at 60k/s). With four clients ordering, killing the receiver and then the sender (-9) left the receiver's count and total equal to what the sender saved, five times and under ASan.
* Send messages as part of events and apply delivered ones once (078)eunhhu2026-09-271-1/+23
| | | | | | | | | | | | | | | | | | | | | | | An output (input kind 5) is a message the app sends; SEND (72) takes an output's field values in order in a handler, lays them out as a message's payload and keeps it, numbered per output from 1, with the event's changes: a failed event sent nothing, and a failed save takes back every send since the last save with its numbers. A host sees only saved sends (pure_app_outbox) and trims what its receiver has saved (pure_app_trim, gone with the next save). pure_app_deliver applies a message from a named source once: a number heard before succeeds without applying, a refused message counts as heard, a failed save forgets what it heard. Sends, numbers and heard numbers are the app's links state, stored whole in one runtime document (PLK1) with the states, in the same commit. Outputs follow their names across module changes; a module that drops or retypes an output with sends left is refused, and a dropped output's number is kept for when it returns. Killing the process at every commit stage leaves an event and its send, or a delivery and its effect, together or neither. pure-serve answers a post to an output or a timer with 404 no_input.
* Choose the connection threads with --threads autoeunhhu2026-09-271-0/+1
| | | | | | | | One connection thread for each CPU the process may run on (its affinity, or the online CPUs), less the one the owner of an app with states takes: three CPUs give two connection threads and the owner. The default stays one thread.
* Gather after a save until a just-answered client has come backeunhhu2026-09-271-5/+7
| | | | | | | | | | | | | After a round that saved, the owner waited for 100 us without a new request (1 ms at most) before the next round. Requests that queued up during the save made that quiet start at once, so the clients just answered missed the next round; the clients then settled into two halves taking turns, each save carrying half of them. It showed with two connection threads: 34 events a round and 15.8-16.0k posts/s with 64 connections, against about 61 and 22-24k/s with one or three. The quiet now counts only once a request has come after the answers were handed back: two threads carry 61.6 events a round, 22.9-23.9k/s.
* Read JSON bodies in place, groups included (082)eunhhu2026-09-271-5/+10
| | | | | | | | | | | | | | | | | | | | | pure-serve no longer copies a JSON body's strings into the message's payload. The ingress is the prefix, a table of each field's start and size, and the body as it came; the connection thread scans the body once, puts numbers into the prefix and records where each string is. Only a string with escapes is unescaped, over itself (it never grows). A group of fields comes as an object inside the body and, for a fixed field, as its path in the query (?to.zip=7). A group where a field belongs, a value where a group does, an array or true/false/null is json_type; an unknown member, inside a group too, json_field. The string scan and UTF-8 checks now pass over eight bytes at a time while none needs a look (pure_utf8_step, so every text check gains). On the Pi, posting {"name":"k","text":"x..."} to a stateless app went from 652-655 to 3,737-3,953 requests/s with 256 KiB of text (server CPU 1.52 to 0.25 ms a request, peak RSS 5.2 to 3.2-3.4 MiB) and from 29.1k to 76.3-76.7k/s with 4 KiB; the load generator gains a json method and benchmarks/http/echo.aup is the app.
* Name message fields by paths and take variable fields where they areeunhhu2026-09-271-4/+5
| | | | | | | | | | | | | | | A message field's name may now be a path of identifiers joined by dots (user.name, a field inside the group user); a module that names both a field and a group (user and user.name) is refused, as are empty, leading, trailing or digit-first segments. A module may have 64 message fields instead of 32 (PureModule 11.9 to 13.2 KiB). pure_app_message_fields takes each variable field's start and size in the payload, so a host that parsed a request in place passes the fields' bytes where they are instead of laying them out after the prefix. The runtime checks them as before: a field past the payload fails with limit, a text that is not UTF-8 with encoding.
* Show views from the last save's snapshot on connection threads (080)eunhhu2026-09-261-9/+18
| | | | | | | | | | | | | | | | | | | | The owner publishes a snapshot of the saved states after every save, before it hands back the answers; connection threads take the newest one each round and show views from it with a reader app of their own. A read made after a write's answer sees the write, nothing unsaved or undone is ever shown, and reads no longer wait while the owner applies events or syncs. Apps with states always get an owner thread, even with one connection thread. A round's inputs go to the owner first; its views are shown and then answered together (answering each one as it was shown made one thread's 20-row page 8% slower). On the Pi 5, with 8 connections posting durable scores and 56 reading the total, reads went from 10-16k/s at p99 13-29 ms to 107-149k/s at p99 0.6-0.7 ms, and posts stayed in range; axum and drogon, which sync under their lock, read 1.1-1.3k/s at p99 111-156 ms. A snapshot of a changed 1,024-item list costs 1.2 us (65,536 items: 87 us) against a 3-4 ms save.
* Snapshot an app's saved states for readers on other threadseunhhu2026-09-261-0/+11
| | | | | | | | | | | | | | | | | | | | | | Right after a save, pure_app_snapshot makes a read-only snapshot of the saved states: numbers are copied, text states are frozen drafts that share the store mapping (pure_draft_freeze, no copy), and lists are copies of their item pointers that share the items. An unchanged list shares the last snapshot's copy. pure_app_read binds a reader app on another thread to a snapshot so the existing view functions read it. A shared item never changes again: a list stamps each item with its share count, copies a shared item it changes, and retires shared items it lets go instead of freeing them. Snapshots form a chain that goes oldest first, and items retired while a snapshot was the newest go with it. References are atomic and may be released on any thread. test-module checks that each snapshot equals the app, that kept snapshots stay as made through changes, removals, a module switch, the app stopping and the store closing, that a failed save's snapshot equals the one before, and that two threads read the newest snapshots while the app goes on (also under ASan, UBSan and LeakSanitizer).
* Lend a text state's stored extents for its view (079)eunhhu2026-09-261-1/+1
| | | | | | | | | | | | A view that shows a text state as it is (pure_module_text_state) now goes out as the state's stored extents, as a document read does: the answer holds the store mapping, charged to the budget, until it is sent and its ticket released; a state with edits not yet stored is copied as before. Documents and views share one loan path, and the extent array grows as needed. A 1 MiB note read by 8 connections: 776-817 answers a second copied, 1,875-1,901 lent, p50 9.2 to 3.5 ms, peak RSS 10.3 to 5.7 MiB. The load generator learns GET /views/page.
* Size a module's code and constants to the module; raise their limitseunhhu2026-09-261-2/+4
| | | | | | | | | | | | | A real app (a board, a stopwatch) already used a quarter of the 512 instructions, since a fn is inlined where it is called, and every text it shows is one of 64 constants. The code and the constants now live in one block sized to the module, which load takes and pure_module_release gives back; the limits rise to 65,535 instructions and 4,096 constants of up to 1 MiB, while PureModule shrinks from 28,664 to 11,936 bytes. The verifier's branch marks are sized to the block instead of 64 KiB on the stack. pure_module_read reads a module file whole, and every host uses it instead of a 128 KiB buffer.
* Tick a served app's timers and give its events the clockeunhhu2026-09-261-0/+3
| | | | | | | | | | pure-serve sets the time an event sees to the wall clock when the event is applied, and the thread that runs an app with states (the connection thread alone, or the owner) ticks its timers on time: epoll waits no longer than the next tick, the owner's condition wait is timed to it, and the ticks due at once share one save, so a restarted server goes on from the stored ticks. The statistics count ticks.
* Add time: timers, now, floor and a floored moduloeunhhu2026-09-261-2/+7
| | | | | | | | | | An input may be a timer (kind 4, its period of 1 ms to a day in place of a key name): pure_app_tick delivers a tick, an event saved like a key's. NOW gives handlers the time the host set in PureApp.now (milliseconds since 1970, UTC), so a script replays any time and views stay functions of state; FLOOR and MOD take times apart, MOD taking the divisor's sign as floor division leaves it, a zero too. pure-run gains time MS and tick NAME.
* Take messages of number and text fields, several variable ones, as JSONeunhhu2026-09-261-3/+7
| | | | | | | | | | | | | | | | | | A message field is now a u32, a number (binary64), bytes or text, and variable fields may be several and anywhere: the payload's prefix holds the fixed fields and each variable field's length but the last one's, which takes the rest; a module whose one bytes field came last is laid out as before. On arrival a length past the payload, a text that is not UTF-8 or a number that is not finite fails the message. pure-serve fills an input's message from a JSON object body (Content-Type: application/json) on the connection thread before the request is ready, so the owner never parses and replies are still borrowed: the ingress is [u32 payload size][payload][the JSON as it came]. A missing, unknown, doubled or mistyped field, or text that is not JSON, answers 400 and the request never runs. Numbers may come in the query too.
* Record the app service against axum and drogon (077)eunhhu2026-09-261-1/+3
| | | | | | | | | | | | The scoreboard, 22 lines of aup served by its views and inputs, against the same service written by hand in axum and drogon on the Pi: durable posts about 76 times as many, reads level with drogon and ahead of axum with the least CPU and memory. The note keeps what the profile showed on the way (the kernel's share of a single thread, integers through printf, an owner that saved too early) and what comes next. run.py gains pure-serve-2 and waits for a server that prints its port before it listens.
* Serve an app with states from several connection threadseunhhu2026-09-261-2/+9
| | | | | | | | | | | | | | | | | | | States have one owner, so a module with them ran on a single thread, which spent most of its time in the kernel sending and receiving. With --threads N such a module now gets N connection threads and one owner thread: connection threads accept, parse and answer; the owner takes the ready requests of every thread as one round, applies them in bursts with one save each and hands each thread back its applied requests (eventfd), which it completes and answers. A request's route and payload stay held by its thread's runtime core until completion, so an accepted event runs even when its connection closes first. After a burst that saved, the owner gathers requests until none has come for 100 µs (1 ms at most), so the requests its answers bring back share the next save as they would on one thread: 30 events per save became 50 and the median latency of a durable post fell from 5.4 to 3.5 ms. A page of 20 rows went from 87,600 to 141,900 answers a second on the Pi.
* Serve an app's views and inputs over HTTPeunhhu2026-09-261-4/+23
| | | | | | | | | | | | | | | | | pure-serve answers GET /views/NAME with a view's value (a number or text as text/plain, a table as JSON rows, a page of them with ?from=&count=, each cell computed only for the rows asked) and POST /inputs/NAME with an event: a text input's text is the body, a key input is pressed, a message takes its u32 fields from the query and its bytes field from the body and answers its handler's reply. A failed event answers with its fault (404 not_found, 409 exists, 422 the others). No routes are written: the app's views and inputs are its interface. Documents stay served when the module has show and edit. The route of a request is kept per work slot, so an accepted event runs even if its connection closes before the burst; views and app events join any burst, whose one save comes before its answers.
* Add keyed lists and text comparisoneunhhu2026-09-261-6/+35
| | | | | | | | | | | | | | | | | | | | | A list state may name a key field of its record (the flags word of its entry: field + 1). A keyed list keeps its items in key order, numbers by value and texts by their bytes, each key once: LIST_APPEND places an item by halving and fails with the new fault exists (PURE_CONFLICT) when the key is there; LIST_FIND gives the index of a key's item and LIST_HAS whether it is there. The verifier refuses inserting by position and changing a key. Maps, sets and sorted maps are one list with an order rule, so tables, filters, folds, indices, storage and replacement stay as they are. A module that opens over a list not in its key order (one that was not keyed, keyed by another field, or stored so) puts it in order, moves its indices with their items and stores it again; two items with one key refuse the module rather than drop one. TEXT_COMPARE compares two texts by their bytes under one of the six relations.
* Add first: the index of the first item a block keepseunhhu2026-09-261-3/+3
| | | | | | | | A fifth fold kind, first, runs its block over the items until one leaves a non-zero number and pushes that item's index, typed as an index of the list, or fails not_found when none does. Handlers can so find the item to change by a condition, as a message naming a task by its title would.
* Name the view entry's list byte in the format tableeunhhu2026-09-261-1/+1
|
* Filter a table's rowseunhhu2026-09-261-2/+8
| | | | | | | | | | | | | | | | | | | | A table view can now carry a filter: its list goes in the byte after the view's type (list + 1) and its code range is a block run as a column of the list, leaving one number; the table has a row for each item the block keeps, in the list's order. Columns read the item (ROW is the item's position, not the row's). The filter runs once per item after a change of the app's states, counted by a new number that never goes back (events, points, a failed save's undo, a module replacement), and its rows serve every cell until the next change. pure_app_row_item and pure_app_item_row turn a table's rows into items and back, so a host shows a focus (an index names an item) on the row it is in and points at the item a clicked row shows. pure_app_rows now takes a mutable app. The checks cover rows, cells, the mapping both ways as items leave and come back, six refusals, and 20,000 damaged filter modules.
* Add folds: counts, sums, maxima and minima over a listeunhhu2026-09-261-1/+6
| | | | | | | | | | | | | | | | FOLD (opcode 65) runs the block that follows it once per item of a list, as a column of that list, and pushes the count of items whose block left a non-zero number, or the sum, maximum or minimum of the values the block left with an include flag; an empty maximum or minimum is not_found. The verifier checks the block as a column (reads only, one or two numbers left, no fold inside, so the work is bounded by block length times items) and treats it from outside as one instruction that nothing branches into. Views and handlers can now say how many tasks are open or what the scores add up to without keeping a second copy of it in state. The checks cover values, empty lists, filters, a fold inside arithmetic, six refusals and 20,000 damaged fold modules run under ASan and UBSan.
* Let a cursor stay before text inserted at it (a mark)eunhhu2026-09-261-2/+2
| | | | | | | | | | | Text inserted at a cursor's position pushed every cursor there behind it. That suits a caret, not a remembered position: a mark, a selection's start or a bookmark should stay before the new text. A cursor state's reserved word in the module now carries a flag: 1 makes it a mark, which stays where text is inserted at it and otherwise follows edits as any cursor does. The flag is refused on other states and other values; existing modules, with 0 there, keep their meaning.
* Let a host point an index at an itemeunhhu2026-09-261-0/+5
| | | | | | | | | | | pure_app_point moves an index state to an item, as a user pointing at a row would: an event with no handler, saved like a key event, joining the burst when saving is deferred; past the last item it is not_found. A host learns which index to move from the view that shows it: pure_module_position gives the state behind a number view that is just `at.position`. An interface can so turn a click on a list row into its focus without any code in the app.
* Charge lent answers to the payload budgeteunhhu2026-09-261-2/+5
| | | | | | | | | | | | | A read answered with extents of the store mapping was charged nothing, so clients that stopped reading could keep any number of old file versions mapped. The runtime core now takes resources a host sends in pieces: body.data is NULL, body.size is charged like any other resource, the outcome has no body to read and cannot be forwarded. pure-serve hands the mapping reference over as such a resource, so a lent answer holds its bytes of budget until it is in the socket and its ticket releases the mapping; past the budget a read gets 503 payload_budget, as a copied one would. The async core grows 52 bytes (5,776 of its 6,144).
* Count a connection's deadline from its last progresseunhhu2026-09-261-1/+2
| | | | | | | | | pure-serve renewed a connection's deadline when a request arrived or an answer finished, not while an answer was being sent, so a client that read a large answer steadily but slowly was cut off after ten seconds (a 1 MiB document over a 50 KB/s link never arrived). Every write that makes progress now renews it; only a stalled connection expires.
* Let a burst hold up to 256 documentseunhhu2026-09-261-2/+2
| | | | | | | | | | | A burst ended once 16 keys had joined it, so 64 writers needed four commits a round and some waited through several (p50 17.4 ms). The pending documents now grow on demand up to 256 keys, which also makes PureApp 1.3 KB smaller (3,128 bytes). With 64 connections a commit now carries 26 events on average instead of 12 and the median PUT waits 9.2 ms; throughput moves less, since each commit writes more and the SD card's syncs take most of the time.
* Compress the module executor's pathseunhhu2026-09-261-5/+24
| | | | | | | | | | | | | | | | | | | The executor shrinks from 49.8 to 42.2 KB of -O2 text (module.c 34.0 to 27.0 KB) with the module format and API unchanged: - The verifier reads its rules from the signature table: what each opcode's argument names, and which text or list it works on. Cursor and index operands must belong to that text or list and so does a pushed one; other values belong to nothing, so a value that merely came from a text no longer blocks a join. The loader and verifier fail through one path. - The interpreter advances as it fetches and leaves through two exits: a stated fault, or a change the data refused. - Key, text and message events take one path; a message brings its base revision and key. Starting an app opens its module over none, the path replacing takes, so every new state starts from its stored value. - Paths that run once per module or app are marked cold and built for size; event paths stay built for speed (small GETs: 128k/s, as before).
* Shard pure-serve across coreseunhhu2026-09-261-1/+4
| | | | | | | | | | | | | | | | | | A burst now looks at the store once (pure_store_pin) instead of once per request; another shard's commit ends before its answer is sent, so a request made after that answer still reads it. With this alone one thread serves 125k small keep-alive reads per second, as many as axum on three cores, with a shorter p99. --threads N runs a shard per thread: its own SO_REUSEPORT listener, epoll set, runtime domain, app and store handle. Writer sessions of the tree store now take turns inside a process with a mutex (the file lock belongs to the process and does not keep threads apart), and revisions settle concurrent edits of a key. A module with states runs on one thread. Three shards serve 151k reads per second; they still commit one by one, so durable writes and 1 MiB reads are better on one thread for now, which stays the default. Note 075 records the numbers.
* Serve modules over persistent connectionseunhhu2026-09-261-18/+19
| | | | | | | | | | | | | | | | pure-serve replaces the module mode of server.c, which goes back to what it was. It keeps the document contract, the runtime core (a work slot per connection, the payload budget, core-owned ingress, borrowed PUT replies) and the group commits, and adds what server.c lacked: HTTP/1.1 persistent connections with in-order pipelining, an edge-triggered epoll owner thread, up to 1,024 connections allocated as they arrive and reused, one writev per answer, and documents opened through a store handle. On the Pi 5 with one thread it serves 106k small keep-alive reads per second (axum on three cores: 126k) at the same p99, 1,741 1 MiB reads (axum: 1,370), and 1,126 durable writes from 64 connections (crash-safe axum: 372), in 7.3 MiB against axum's 17-20 MiB. Note 075 has the table.
* Serve modules through the runtime core with group commitseunhhu2026-09-261-1/+20
| | | | | | | | | | | | | | | | pure-connections-module offers the document server's HTTP contract with an aup module: GET is its show message and PUT its edit message. Requests go through the runtime core as before (work slots, payload budget, ingress, replies borrowed from the request), and effects are taken on the owner thread with the core's asynchronous interface. Each poll round is one burst: every ready request is applied to the app with deferred saving, one save stores them, and only then are the tickets completed and answered. A failed save fails the whole burst, reads too. A key seen again ends the burst first, and the module server accepts every waiting connection at once so concurrent requests share a burst. On the Pi's SD card eight clients' 240 edits take 0.59 s instead of 2.14 s.
* Save app changes in burstseunhhu2026-09-251-8/+17
| | | | | | | | | | | | | | | | | | | A save now publishes everything since the last save in one commit: the documents messages edited and the states that changed. If it fails, every event since the last save is undone (values, text and list drafts, the list undo log, event and tick counts), so an event reported as successful is stored and a failed one leaves no trace. By default each key, text and message event saves before it returns, as before. With pure_app_defer events apply at once and a host saves a burst with one commit; a key's document joins a burst once, so revisions move one per save. On the Pi's SD card 600 events take 6.3 s saved one by one, 0.97 s in bursts of 8 and 0.34 s in bursts of 64. pure-run takes --batch N. Tests compare a deferred stored app that saves at random and sometimes stops unsaved with an app that replays only the saved events, undo a whole burst on a failed save, and kill a process while it saves a burst.
* Keep app states in the tree storeeunhhu2026-09-251-3/+34
| | | | | | | | | | | | | | | | | | | | | | | When the data host keeps states (PureData.state and .save, implemented by pure_tree_data), every state of an app lives in the store: a text state's draft is its stored value, a list mirrors each change into a stored draft (record header, then items), and numbers, cursors and indices are stored together by name. An app starts from what is stored, moves list fields by name when a record changed, resets cursors and indices that no longer fit, and refuses damaged stored values. Each key, text and message event publishes the states it changed together with the message's document in one store commit, or fails and leaves memory and store as they were. Frame changes go with the next event or pure_app_save. pure-run takes --store PATH. Tests run the same random events on an app in memory and on a stored app that restarts every few events and require equal states after every step; kill a process at every commit stage of an event that changes a number, a text and a list; and check frames, message atomicity and migration by restart and by replacement. A stored event costs about 10.6 ms more on the Pi's SD card, mostly two fsyncs.
* Add stack copies to Pure moduleseunhhu2026-09-251-1/+4
| | | | | | | | | PICK copies the value n below the top, with its type and owner; SLIDE drops n values under the top. A toolchain can compute a function's arguments once and read each as often as the inlined body needs, instead of repeating the argument's code. The format stays v5: no producer has shipped modules without them.
* Add conditional joins and table views to Pure moduleseunhhu2026-09-251-12/+22
| | | | | | | | | | | | | | | | Module format v5 lets a value cross a join: the verifier records the stack each branch brings to its target and requires every path to agree on height, types and owners, with at most 8 such joins ahead. Branches are now allowed in views, so a view can be a conditional expression. A table view has one row per item of a list. Its columns are number or text views evaluated for one row at a time and may read that row (ROW). Hosts compute only the cells they show through pure_app_rows, pure_app_cell and pure_app_cell_text. Column names are scoped to their table. The view limit rises to 64, columns included. pure-run prints a table view as an array of row objects.
* Add records, lists and indices to Pure moduleseunhhu2026-09-251-16/+45
| | | | | | | | | | | | Module format v4 declares record types and adds list states (items of a record) and index states (a position in one list). The verifier types record operands field by field and rejects an index used on another list. Indices follow their item across inserts and removes. List changes are logged per event and undone when the event fails. Replacing a module keeps a list by name and moves its fields by name when the record changed. pure-run prints lists as arrays and a failed view as null.
* Record the module size after raising the input limiteunhhu2026-09-251-1/+1
|
* Allow 32 inputs per moduleeunhhu2026-09-251-1/+1
| | | | | | | A text editor needs more keys than the workbench-era limit of 8: typing, four arrows, Backspace, Delete, Home, End and a mark already make 10. Event numbers stay 1 + 2i and 2 + 2i; the module grows by the input name tables.
* Add text states and cursors to Pure moduleseunhhu2026-09-251-17/+46
| | | | | | | | | | | | | | | | | | | | | | Module format v3 adds text states, cursors, UTF-8 constants, text inputs and text views. A text state is a runtime-owned draft without a store, so a large text edits as an extent tree. Cursors are byte positions the runtime keeps on character boundaries: insertion pushes cursors at or after the point, removal gathers cursors inside the range, and a failed event returns every text draft and cursor to where it was. The verifier types a cursor by the text it belongs to and rejects editing one text with another's cursor. Operations cover constants, concatenation, shortest round-trip number text, character, line and byte counts, character and column-keeping line moves, insert, remove, line views, whole-text replacement and UTF-8 checked conversion from bytes. Replies of edited documents or texts are copied before they are finished. pure-run gains text input and text output. test-module covers typing Korean and emoji, moves that stop at the ends, line moves, views, rollback, invalid UTF-8 and replacement, text-specific verifier rules, and 20,000 damaged text modules whose cursors always stay on character boundaries under ASan/UBSan.
* Add typed messages and keyed documents to Pure moduleseunhhu2026-09-251-40/+59
| | | | | | | | | | | | | | | | | | | | Module format v2 adds byte values, message inputs with u32 and bytes-tail fields, one keyed byte collection, replies and require. The verifier checks every operand's type and the context each operation may run in, and allows branches and joins only at an empty stack, so typed values never merge. A message event opens its key's working version through a PureData host, conflicts early on a stale base revision, runs its handlers, and commits only if they succeed; a failed event leaves documents and states unchanged. Replies lend payload or document extents, and a document reply is copied before commit. pure_tree_data implements the host on tree store drafts, and pure-edit-module offers the document CLI contract of pure-edit from a module. test-module adds document semantics on an in-memory host (not found, create, conflict before bounds, limits, short payloads, busy commits, stable replies, require, state rollback), typed verifier rules, and 20,000 damaged message modules run safely under ASan/UBSan.
* Define Pure module format v1 with a static verifiereunhhu2026-09-251-0/+72
| | | | | | | | | | | | | | | Pure now owns the format its toolchains target, as the JVM owns class files. A v1 module carries f64 states, key inputs, atomic event handlers and pure views. pure_module_load checks sizes, names, keys, operands, forward-only branches, reachability and stack heights at every join before anything runs, so execution needs no stack checks and each event is bounded by its handler length. PureApp runs events on a pending copy and replaces modules keeping states by name, without allocation. Semantics match the workbench engine, including IEEE minimum/maximum on signed zeros. test-module covers behaviour, atomic faults, 26 rejection checks and 20,000 damaged modules under ASan/UBSan; pure-run is a JSON test host.
* Establish native micro runtime checkpointeunhhu2026-09-2410-0/+1010