Compare commits

...
10 Commits
381 changed files with 320593 additions and 1936 deletions
+54 -17
View File
@@ -11,9 +11,10 @@ UCE is a PHP-inspired server-side runtime that lets you build web pages and hand
- `.uce` pages compile to shared objects on demand
- normal HTTP pages expose `RENDER(Request& context)`
- WebSocket pages can additionally expose `WS(Request& context)`
- sub-rendering and components pass structured data through `context.call`
- local CLI/admin/test entrypoints can expose `CLI(Request& context)` and are invoked through the Unix CLI socket
- sub-rendering and components pass structured data through `context.props`
- nginx can forward normal `.uce` requests and ordinary `.ws.uce` page loads to the FastCGI socket, while real WebSocket upgrade requests for `.ws.uce` endpoints go to the built-in HTTP/WebSocket listener
- the nginx-published application tree now lives under `site/`
- the nginx-published application tree lives under `site/`
- you can include C++ code as much as you want, but only .uce files called via API functions and entry points will be pre-processed
- the preprocessor has two jobs:
- allow for inline HTML within C++ and the use of templating tags inside of that HTML
@@ -42,8 +43,11 @@ The current build expects:
- `clang++`
- `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- standard Linux development headers for `dl`, `pthread`, sockets, and backtrace support
SQLite is vendored under `src/3rdparty/sqlite/` and compiled by `scripts/build_linux.sh`; no system SQLite package is required.
The binary is written to:
```bash
@@ -52,21 +56,25 @@ bin/uce_fastcgi.linux.bin
## Runtime Model
UCE pages now use explicit request handlers instead of implicit globals:
UCE pages use explicit request handlers instead of implicit globals:
- `RENDER(Request& context)` for normal HTTP rendering
- `WS(Request& context)` for inbound WebSocket messages
- `CLI(Request& context)` for local command-line/admin/test invocations through `CLI_SOCKET_PATH`
Useful related runtime patterns:
- `unit_render(String file_name)` or `unit_render(String file_name, Request& context)` to invoke another page
- `context.cfg` for request-local structured configuration
- `context.call` for invocation or message-local structured input
- `context.props` for invocation-local structured input such as component props
- `context.connection` for broker-owned per-WebSocket-connection state shared across `WS(Request& context)` calls
- `context.params["UCE_CLI"] == "1"` while handling a local CLI socket request
- `context.in` for the current request body, including the current WebSocket message payload inside `WS(Request& context)`
- `context.params["WS_..."]` for direct WebSocket message metadata on the request parameter map
- `context.params`, `context.get`, `context.post`, `context.cookies`, `context.session`, and `context.header` for request/response state
- `context.set_status(code[, reason])` to set the HTTP response status
Useful helpers for that data model now include:
Useful helpers for that data model include:
- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys
- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended
@@ -74,6 +82,11 @@ Useful helpers for that data model now include:
- `json_encode(String)` for emitting JavaScript-safe string literals directly
- `ascii_safe_name(String)` for conservative ASCII identifier normalization
- `path_join(base, child)` for filesystem-style path assembly
- `sqlite_connect()`, `sqlite_query()`, and related helpers for embedded SQLite storage with named prepared parameters
- `zip_create()`, `zip_list()`, `zip_read()`, and `zip_extract()` for minimal ZIP archive workflows
- `gz_compress()` and `gz_uncompress()` for gzip-format byte strings
- `server_start_http()` / `server_stop()` for runtime-managed custom HTTP listeners backed by `SERVE_HTTP` handlers
- `map()`, `filter()`, `list_unique()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code
Named component handlers are also supported:
@@ -81,16 +94,31 @@ Named component handlers are also supported:
COMPONENT:BODY(Request& context)
{
<>
<p><?= context.call["body"].to_string() ?></p>
<p><?= context.props["body"].to_string() ?></p>
</>
}
```
Those are intended for sub-rendering through helpers such as `component("components/card:BODY", props, context)` rather than direct page entry.
Additional lifecycle hooks are also available on ordinary `.uce` units:
- `INIT(Request& context)` runs once when a worker loads that unit's shared object into memory
- `ONCE(Request& context)` runs once per request before the first `RENDER()`, `CLI()`, or `COMPONENT...` entrypoint from that file
CLI units can be invoked locally with the convenience wrapper or directly over HTTP-over-Unix:
```bash
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
```
For structured CLI commands, prefer JSON POST bodies and read them with `cli_input(context)`.
## Template Output
UCE now treats template parsing as one shared code-vs-literal state machine.
UCE treats template parsing as one shared code-vs-literal state machine.
- `<>` and `?>` both enter literal output mode
- `</>` and `<?` both return to code mode
@@ -104,9 +132,9 @@ Inside literal output, UCE supports three inline forms:
Use `<?= ... ?>` by default for user-visible text. Use `<?: ... ?>` only for trusted markup or content that has already been escaped.
The parser now treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
The parser treats C++ `//` and `/* ... */` comments as comments in both normal code and `<? ... ?>` islands, so quotes or delimiter markers inside comments do not confuse template parsing.
The preprocessing implementation is now split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
The preprocessing implementation is split between `src/lib/compiler.cpp` and `src/lib/compiler-parser.cpp`. `compiler.cpp` owns unit compilation and cache orchestration, while `compiler-parser.cpp` owns source rewriting and template parsing.
## Components
@@ -117,7 +145,7 @@ UCE includes a native component layer built on top of ordinary `.uce` files:
- `component_exists(name)`
- `component_resolve(name)`
Component props are passed through `context.call`.
Component props are passed through `context.props`.
Component names resolve:
@@ -139,6 +167,8 @@ Components expose `COMPONENT(Request& context)` as their default entrypoint and
The component helpers call only `COMPONENT...` handlers. A file meant purely for component use can define `COMPONENT()` without defining `RENDER()`, which keeps direct page entry and component entry cleanly separated. Inside a component file, `component(":NAME", props, context)` and `component_render(":NAME", props, context)` target another named component handler in that same file.
If the component file also defines `ONCE(Request& context)`, that hook runs once per request before the file's first component/render entrypoint. If it defines `INIT(Request& context)`, that hook runs once when the worker loads the unit.
## WebSockets
The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate API to page code:
@@ -156,17 +186,19 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate
By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint.
Each live WebSocket connection now owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client.
`ws_message()` may contain either text or binary payload data. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`.
`ws_message()` may still be used when you want the payload through a helper API. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type.
Set `binary = true` on `ws_send()` or `ws_send_to()` to queue a binary frame instead of a text frame.
The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
The runtime accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS(Request& context)`.
## Error Reporting
Unhandled exceptions and recovered fatal request signals now return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
Unhandled exceptions and recovered fatal request signals return a `500 Internal Server Error` response with a plain-text trace instead of simply dropping the upstream connection and leaving nginx to show a generic `502`.
The demo page `site/test/error-reporting.uce` can be used to exercise:
@@ -178,15 +210,19 @@ The current error page includes:
- request URI
- resolved script path
- generated C++ path when available
- high-level error summary
- source/generated excerpts and raw compiler output paths for template/component/unit failure modes
- signal number and name when applicable
- a native backtrace
Compile failures are also formatted with the source path, generated C++ path, compile-output artifact path, a nearby source/generated excerpt when a line can be identified, and the raw compiler output.
This recovery path currently covers normal request handling. It is not yet the universal recovery path for every runtime subsystem.
## Docs And Tests
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`.
The most current user-facing reference lives under `site/doc/`, and the demo pages live under `site/test/`. Developers coming from React, Next, or Remix should start with `site/doc/pages/coming_from_react.txt` / `/doc/index.uce?p=coming_from_react` for the concept map and starter-router notes.
Useful entry points:
@@ -228,7 +264,7 @@ On a Debian or Ubuntu host, start with the packages needed to build and run UCE
```bash
apt update
apt install -y nginx clang mariadb-client libmariadb-dev build-essential
apt install -y nginx clang mariadb-client libmariadb-dev libpcre2-dev build-essential
```
The exact package names may vary by distro. The important requirements are:
@@ -236,6 +272,7 @@ The exact package names may vary by distro. The important requirements are:
- `nginx`
- `clang++`
- `mysql_config`
- PCRE2 development headers and library (`libpcre2-dev` on Debian / Ubuntu)
- normal Linux development headers for threads, sockets, `dl`, and backtrace support
### 2. Put the repo on the server
@@ -480,7 +517,7 @@ Common failure modes:
- WebSocket upgrade fails
Check that nginx is routing `.ws.uce` to `proxy_pass`, not `fastcgi_pass`, and that `HTTP_PORT` is reachable on localhost.
- Requests compile but immediately crash
Check `journalctl -u uce.service`. Generated units now carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
Check `journalctl -u uce.service`. Generated units carry an ABI metadata sidecar and should be recompiled automatically after runtime ABI changes, but clearing stale artifacts under `BIN_DIRECTORY` is still a useful last-resort recovery step if the cache has been damaged manually.
- nginx serves raw source or internal files
Tighten the server root and add explicit deny rules for non-public directories.
+492
View File
@@ -0,0 +1,492 @@
# UCE Code Review — Full Findings (2026-06-11)
Scope: the pending working-tree changes (73 modified tracked files plus new
untracked sources — notably `src/lib/sqlite-connector.cpp/.h` and the
uce-starter theme/router rework).
Method: seven independent review angles (line-by-line diff scan,
removed-behavior audit, cross-file call tracing, reuse, simplification,
efficiency, altitude), followed by a verification pass on the correctness
candidates. Status legend:
- **Confirmed** — verified against the working tree, decisive lines quoted.
- **Plausible** — surfaced by a review angle, not individually re-verified.
- **Refuted** — investigated and found not to be an issue (kept here so
nobody re-flags it).
---
## Part 1 — Correctness (all confirmed)
### 1.1 XSS: island props break out of single-quoted attribute
**File:** `site/examples/uce-starter/components/theme/web_affordances.uce:12`
The island component emits JSON props inside a single-quoted attribute
(`data-props='<?: html_escape(payload) ?>'`), but `html_escape()`
(`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` /
`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a
prop value terminates the attribute.
**Failure:** a view passes user-influenced prop text containing an apostrophe,
e.g. `x' autofocus onfocus='alert(1)` — the attribute terminates early and
attacker-controlled attributes/event handlers land on the div. Even benign
values like `Don't` silently truncate the payload.
**Fix:** use a double-quoted attribute (html_escape covers `"`), or extend
`html_escape` to escape `'` as `&#39;`.
**Status:** fixed, but fix not verified yet.
### 1.2 Crash: positional `?` placeholders in sqlite_query
**File:** `src/lib/sqlite-connector.cpp:86`
`bind_params` constructs a `String` directly from
`sqlite3_bind_parameter_name(stmt, i)`, which returns NULL for nameless
positional `?` parameters. `std::string(nullptr)` is UB and crashes before the
`if(name == "") continue;` guard on the next line can run.
**Failure:** any page calling
`sqlite_query(db, "select * from notes where id = ?", params)` with a bare `?`
instead of `:name` segfaults the worker (500 recovery page).
**Fix:** remove support for positional placeholders from sqlite and mysql API in favor of named placeholders, adjust the documentation accordingly.
**Status:** fixed, but fix not verified yet.
### 1.3 HTTP parsing: split_http_headers assumes lines[0] is the request line
**File:** `src/lib/functionlib.cpp:754`
The rewritten parser unconditionally treats the first line as the HTTP request
line. The old parser located the first colon-free line, which tolerated a
leading CRLF (RFC 7230 §3.5) and header-only input. Two failure modes:
- The direct-HTTP caller (`src/fastcgi/src/fcgicc.cc:693`) passes the raw
buffer unstripped — a client sending a stray leading CRLF before
`GET /x.uce HTTP/1.1` gets the request line silently dropped (empty
`REQUEST_METHOD`, request fails).
- Header-only input (`Host: example.test\nX-Token: abc`, e.g. page code
parsing a raw header block) parses `Host:` as `REQUEST_METHOD` and loses
`HTTP_HOST` entirely. The function is directly callable from .uce pages.
**Fix:** skip leading empty lines before consuming the request line, and only
treat a colon-free first line as the request line.
**Status:** fixed, but fix not verified yet.
### 1.4 Silent data loss: multi-statement SQL drops everything after the first `;`
**File:** `src/lib/sqlite-connector.cpp:178`
`SQLite::query` passes `0` for `pzTail` to `sqlite3_prepare_v2` and never
checks for remaining SQL, so multi-statement strings execute only the first
statement and still report `ok`.
**Failure:** a migration page runs
`sqlite_query(db, "create table t(id integer); insert into t values(1);")` —
only the CREATE executes, the INSERT is silently dropped, `sqlite_error()`
says `ok`, leaving the database half-migrated with no error signal.
**Fix:** capture `pzTail` and either loop over remaining statements or raise
an error when trailing SQL is present.
**Status:** fixed, but fix not verified yet.
### 1.5 Diagnostics regression: fault backtrace captured after siglongjmp
**File:** `src/linux_fastcgi.cpp:895`
The in-handler `capture_backtrace_string` call (`request_fault_trace`) was
deleted; the trace is now captured after `siglongjmp` back in
`handle_complete`, where the faulting stack has already been unwound. No
capture remains in `on_request_fault_signal`.
**Failure:** a `.uce` page null-derefs → SIGSEGV → the error page's Trace
shows only `handle_complete`/`main` frames. The faulting unit's frames are
gone, making crash reports undiagnosable beyond the signal number (the README
still advertises a native backtrace of the failure).
**Fix:** restore the capture inside `on_request_fault_signal` (on the faulting
stack) and hand the result across the longjmp, as the previous code did.
**Status:** fixed, but fix not verified yet.
### 1.7 Memory leak: SQLite wrapper objects never freed by request cleanup
**File:** `src/lib/sqlite-connector.cpp:248`
`cleanup_sqlite_connections()` closes the raw `sqlite3` handles tracked in
`resources.sqlite_connections` but never deletes the heap-allocated `SQLite`
wrappers from `sqlite_connect` (`new` without `delete`; no arena allocator is
compiled in). `sqlite_disconnect` itself is correct (`delete db` after
unregistering). Note: this mirrors a pre-existing identical leak in the MySQL
connector (`cleanup_mysql_connections`, `mysql-connector.cpp:298-304`).
**Failure:** a page calls `sqlite_connect()` per request and relies on
end-of-request cleanup instead of `sqlite_disconnect()` — exactly what the
cleanup path exists for. One wrapper leaks per request; unbounded RSS growth
in the long-lived FastCGI worker.
**Fix:** track the wrapper objects (not raw handles) in
`resources.sqlite_connections` and delete them in cleanup; fix the MySQL
connector the same way, or factor a shared registry (see 5.1).
**Status:** fixed, but fix not verified yet.
### 1.8 Asset shims emit stylesheets mid-`<body>`
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(also `theme_assets.uce`, `gauges/assets.uce`)
The new ONCE-based asset shims fire inside the view's `ob_start()` capture in
`index.uce` (lines 67-85: view is captured into `fragments["main"]`, then
`themes/page` renders), so their `<link rel="stylesheet">` output is baked
into the main fragment and spliced into the content div — inside `<body>`,
not `<head>`.
**Failure:** every marketing/theme/gauges page ships its stylesheet mid-body
(FOUC, invalid-ish markup). The deleted `page_shell` asset registry rendered
these in `<head>`. While this behavior is often okay in practice, we should
improve on this to encourage cleaner output.
**Fix:** Part A: restore a registration mechanism: record component and asset output
that's intended as once per page in context.call["fragments"]["once"] by default
and the page template component can then explicitly slot this in where appropriate.
Part B: Modify the preprocessor so directives support attributes like this:
ONCE(Request& context)
@fragment my-fragment-name
{
...
}
Which will then automatically (in this example) slot the output into
context.call["fragments"]["my-fragment-name"] instead of the default slot name "once".
In the future we'll introduce more attributes with this syntax. Using this flexible mechanism for the fragment slot
name, we can leave it up to the page template where to slot in what. ONCE, COMPONENT,
and RENDER should support the fragment attribute.
**Status:** fixed, but fix not verified yet.
### 1.9 Error page "Generated C++" hint prints a nonexistent path
**File:** `src/linux_fastcgi.cpp:79`
`render_request_failure` computes the hint as
`path_join(BIN_DIRECTORY, SCRIPT_FILENAME) + ".cpp"`, but `path_join` returns
an absolute child unchanged (`src/lib/sys.cpp:179`), while the compiler builds
the real path by plain concatenation `BIN_DIRECTORY + src_path`
(`src/lib/compiler.cpp:631-636`). SCRIPT_FILENAME is always absolute in the
FastCGI path.
**Failure:** every runtime failure page prints e.g.
`Generated C++: /var/www/site/page.uce.cpp` — the BIN_DIRECTORY prefix is
silently dropped and the path never exists (real file:
`/var/cache/uce/work/var/www/site/page.uce.cpp`).
**Fix:** export one helper from `compiler.cpp` that maps a source file to its
generated artifact (`su->pre_path + "/" + su->pre_file_name` — the new
`compiler_format_compile_failure` in this same changeset already computes it
correctly) and use it in both places.
**Status:** fixed, but fix not verified yet.
### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter
**File:** `src/lib/functionlib.cpp:182`
Both helpers call `tree.is_list()` inside the per-element callback —
`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the
operation O(n²) — even though both already compute `is_list()` once before
the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback
element by value (`DTree t`), deep-copying every subtree per iteration.
**Failure:** mapping over a 1000-row `sqlite_query` result does ~1M
key-validation checks plus a full deep copy of each row tree.
**Fix:** hoist `is_list()` into a `bool` local reused in the callback; change
`each()` to pass `const DTree&`.
**Status:** fixed, but fix not verified yet.
### 1.11 Proactive compiler child can std::terminate on lock failure
**File:** `src/lib/compiler.cpp:288`
`compiler_with_registry_lock` now throws `std::runtime_error` when the
registry lock file can't be opened (previously: warning + proceed unlocked).
`run_proactive_compiler` (`src/linux_fastcgi.cpp:1270-1372`) calls
`compiler_list_known_units` / `compiler_set_known_units` with no try/catch
anywhere in the chain, so a transient failure (fd exhaustion, disk full)
aborts the forked child via `std::terminate`.
**Blast radius is small:** the proactive compiler runs in a forked child that
the main loop respawns each iteration, and request-path callers are protected
by the try/catch in `handle_complete`. Still, a catch-and-log around the
proactive scan is cheap insurance.
**Status:** fixed, but fix not verified yet.
---
## Part 2 — Reuse / duplication (plausible unless noted)
### 2.1 request_query_path() duplicates request_query_route()
**File:** `src/lib/uri.cpp:311`
`request_query_path()` copy-pastes `request_query_route()`'s (line 325)
first-keyless-segment scan verbatim — two identical "find first &-part
without =" loops plus `route_path_sanitize` calls that must be kept in sync.
It also has **zero callers** in `src/` or `site/` (only a doc page references
it).
**Fix:** implement as
`return request_query_route(context, default_path)["l_path"].to_string();`.
**Status:** fixed, but fix not verified yet.
### 2.2 list_filter / list_map re-implement the generic filter<T> / map templates
**File:** `src/lib/functionlib.cpp:71`
`StringList` is `typedef std::vector<String>` (`types.h:52`), so the existing
`filter<T>` template (`functionlib.h:68`, declared ~10 lines above the new
`list_*` declarations) already does exactly what `list_filter` does. Two
filter implementations now live in the same module; behavior fixes must be
applied twice.
**Fix:** drop it and point the doc page at `filter`); same consideration for `list_map`.
**Status:** fixed, but fix not verified yet.
### 2.3 Hand-rolled redirects instead of the redirect() helper
**Files:** `site/examples/uce-starter/views/account/login.uce:13`,
`logout.uce:6`, `profile.uce:8`, `site/demo/sqlite.uce:21-22`
Four call sites inline `context.set_status(302, "Found");
context.header["Location"] = ...` instead of calling the existing runtime
helper `redirect(String url, s32 code = 302)` (`src/lib/uri.cpp:414`). This
replaced the single `app_redirect()` helper the changeset deleted.
**Note — security aspect refuted (see 6.1):** headers are sanitized centrally
on write-out, so this is a pure reuse nit, not a vulnerability.
**Fix:** use `redirect(app_link("account/profile", context))` or restore one
`app_redirect` wrapper.
**Status:** fixed, but fix not verified yet.
### 2.5 Asset tag boilerplate repeated across ~a dozen files
**Files:** `components/example/marketing_assets.uce:5`,
`components/example/theme_assets.uce`, `components/gauges/assets.uce`,
plus inline `<link rel="stylesheet" href="<?= app_asset_url(...) ?>" />`
blocks in `components/theme/head.uce`, `components/data/widgets.uce`,
`components/workspace/primitives.uce`, `views/dashboard.uce`
About a dozen hand-written copies of the stylesheet/script tag pattern around
`app_asset_url()`; a versioning or attribute change (defer/integrity) touches
every file. The marketing and theme shims differ only in one CSS path.
**Fix:** collapse the three shims into one parameterizable assets component.
**Status:** fixed, but fix not verified yet.
---
## Part 3 — Simplification (plausible)
### 3.1 request_query_route() emits a derivable "rejected" flag
**File:** `src/lib/uri.cpp:347`
The route tree contains both `route["valid"]` and `route["rejected"]` where
`rejected` is exactly `!valid`, and nothing in the codebase reads
`"rejected"`. Redundant derivable state doubles the invariant surface — a
future path that sets one flag without the other produces a route tree that
lies.
**Fix:** drop the `"rejected"` key; callers needing it can write
`!route["valid"].to_bool()`.
**Status:** fixed, but fix not verified yet.
### 3.2 Starter router carries write-only diagnostic state
**File:** `site/examples/uce-starter/index.uce:38`
The router rewrite adds candidate `kind`/`matched` fields and
`context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which
is read by any component, theme, or test. It also uses `dtree_filter` to
`file_exists`-stat every candidate after the first match is already found.
~55 lines plus a builder helper replace what the deleted `app_resolve_view`
did with three early-return `file_exists` checks.
**Fix:** remove unused parts.
**Status:** fixed, but fix not verified yet.
### 3.4 Single-link shim components where a direct ONCE block suffices
**File:** `site/examples/uce-starter/components/example/marketing_assets.uce:8`
(and `theme_assets.uce`)
These are single-`<link>` shim files with empty COMPONENT bodies, invoked via
`print(component(...))` of an empty string. Routed views are mutually
exclusive per request, so the cross-unit dedup the shims provide can never
trigger; they exist only to host one ONCE line at the cost of two extra files
and an indirection on every view. `dashboard.uce:3-6` already demonstrates
the simpler form (ONCE block directly in the view). `gauges/assets.uce` is
the justified case (multiple sibling components per page) and can stay.
**Status:** fixed, but fix not verified yet.
### 3.5 Compile-failure artifact file is self-referential
**File:** `src/lib/compiler.cpp:847`
`compile_shared_unit()` overwrites `su->compiler_messages` with the formatted
failure report, then writes that report into `compile_output_file_name` — the
same artifact the report's own "Compile output:" line
(`compiler_format_compile_failure`, line 800) points readers at. No copy of
the raw, unformatted compiler output survives for tooling to parse.
**Fix:** keep raw messages as the stored/recorded artifact content and format
only at the print/response boundary.
**Status:** fixed, but fix not verified yet.
---
## Part 4 — Efficiency (plausible unless noted)
### 4.1 QUERY_STRING parsed twice per request
**File:** `src/linux_fastcgi.cpp:744`
`prepare_request_body_maps` calls `request_populate_context_params` (which
splits QUERY_STRING on `&` + uri-decodes inside `request_query_route`) and
then `parse_query(QUERY_STRING)` re-splits and re-decodes the identical
string on the next line. Runs on every HTTP request, CLI invocation, and
websocket event. Route params are also computed eagerly for requests that
never read `ROUTE_*`/`BASE_URL`.
**Fix:** parse QUERY_STRING once into `request.get` first and derive the
route token from that single pass, or populate the route params lazily.
**Status:** fixed, but fix not verified yet.
### 4.2 Repeated normalization in request_populate_context_params
**File:** `src/lib/uri.cpp:351`
Per request: `route_path_normalize` runs three times on the same path
(directly, inside `route_path_sanitize`, and inside `route_path_is_safe`),
`request_script_url` is computed twice (for SCRIPT_URL and again inside
`request_base_url`), and `route_path_normalize` (line 255) strips slashes via
`path = path.substr(1)` in a while loop — O(n²) copies per slash run.
**Fix:** normalize once and pass the normalized string down; compute
script_url into a local used by both params; replace the substr loops with
`find_first_not_of("/")` / `find_last_not_of("/")` and a single substr.
**Status:** fixed, but fix not verified yet.
### 4.4 collect_rows rebuilds column-name strings for every row
**File:** `src/lib/sqlite-connector.cpp:118`
For R rows × C columns the row loop does R×C `sqlite3_column_name` calls plus
R×C String heap constructions and map inserts keyed on the fresh string,
though column names are invariant across rows. A 10k-row, 8-column result
builds 80k redundant name strings per query.
**Fix:** build a `std::vector<String> names(column_count)` once before the
step loop and index it inside (`row[names[i]]`).
**Status:** fixed, but fix not verified yet.
### 4.5 bind_params copies the params map twice and binds SQLITE_TRANSIENT
**File:** `src/lib/sqlite-connector.cpp:95`
`SQLite::query` takes `StringMap` by value and passes it by value again to
`bind_params`, then binds with `SQLITE_TRANSIENT`, forcing SQLite to memcpy
each value a third time — even though the copied map outlives the statement
(it lives until after `sqlite3_finalize`).
**Fix:** pass `const StringMap&` through `query()`/`bind_params` and bind
with `SQLITE_STATIC` (or at least drop the two by-value map copies).
**Status:** fixed, but fix not verified yet.
### 4.6 Per-request connection opens re-run pragmas; busy_timeout set twice
**File:** `src/lib/sqlite-connector.cpp:15`
`connect()` calls `sqlite3_busy_timeout(5000)` and then
`apply_default_pragmas` runs `PRAGMA busy_timeout = 5000` again (pure
duplicate). `PRAGMA journal_mode = WAL` is persistent in the database file
but re-issued on every open. Because cleanup closes all handles at request
end, a page like `site/demo/sqlite.uce` pays `sqlite3_open_v2` + 4 pragmas +
`CREATE TABLE IF NOT EXISTS` on every request.
**Fix:** drop the redundant busy_timeout pragma; cache open
connections per worker keyed by path across requests (resetting state at
request end) so pragmas and schema checks run once per worker.
**Status:** fixed, but fix not verified yet.
---
## Part 5 — Altitude / design (plausible unless noted)
### 5.3 Test suites hand-registered in three parallel lists — already drifting
**Files:** `site/tests/index.uce:15`, `tests/plugins/uce_site_suite.py`
A test page must be registered in three places: the `site/tests/*.uce` file
itself, a `site_tests_card()` line in `index.uce`, and a tuple in
`uce_site_suite.py`. The new `sqlite.uce` was added to all three by hand. The
lists have already drifted: `site/tests/call_helpers.uce` exists on disk but
appears in neither `index.uce` nor any plugin list, and
`security_headers.uce` is covered only by the security-smoke plugin, not the
index cards — new suites can silently fall out of the dashboard and/or CI.
**Fix:** enumerate `site/tests/*.uce` with ls()/glob in both `index.uce` and
`uce_site_suite.py`, with title/tags metadata declared once (in the test page
or a single shared manifest).
**Status:** fixed, but fix not verified yet.
### 5.4 compiler_developer_hints() couples the runtime to clang's English message text
**File:** `src/lib/compiler.cpp:778`
The hint table pattern-matches hardcoded English clang diagnostic substrings
("no member named", "expected ';'", ...). A clang version bump, a switch to
gcc (COMPILE_SCRIPT is user-configurable server config), or localized
diagnostics silently degrades every hint to the generic fallback, and each
new error class means hand-extending an if-chain in runtime C++.
**Fix:** the excerpt mechanism added in the same change
(`compiler_format_compile_failure`'s source/generated excerpts + artifact
paths) already carries the diagnostic value; make the hints a data-driven
table or drop them in favor of the excerpts.
**Status:** fixed, but fix not verified yet.
### 5.5 Generated-artifact path computed by hand in two places
**File:** `src/linux_fastcgi.cpp:79` vs `src/lib/compiler.cpp:631-636`
The deeper-fix framing of 1.9: the moment the compiler's artifact layout or
naming changes (hashing, per-config subdirs), any hand-recomputed path goes
stale. One exported source-file → artifact-path helper in `compiler.cpp`,
used by both the compile-failure formatter and the runtime failure page.
**Status:** fixed, but fix not verified yet.
+496
View File
@@ -0,0 +1,496 @@
# WASM-PROPOSAL: WebAssembly Unit Runtime for UCE
- **Status:** proposal / design draft (2026-06-11)
- **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` →
`dlopen`) with per-unit WebAssembly modules executed in a per-request,
runtime-linked workspace, exposing the same API surface to page code.
- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the
earlier per-invocation arena attempt (preserved in
`src/lib/_scratchpad.cpp`).
---
## 1. Motivation
Two long-standing structural problems and one strategic opportunity share a
single root cause: **request code shares an address space and an allocator
with the runtime.**
1. **The arena attempt failed for a structural reason.** The
`GLOBAL_ARENA_ALLOCATOR` design in `_scratchpad.cpp` swapped the global
`operator new`/`delete` against a `current_memory_arena`. A single global
allocator cannot distinguish request-lifetime allocations from
process-lifetime ones: during a request, server-lifetime structures
(compile registry, sessions, config trees, unit statics) also allocate.
Under the arena those dangle on reset; with `delete` as a no-op,
system-allocated objects released mid-request leak. The lifetime
distinction lives in the type system and call graph, not at the allocator
boundary — fixing it in-process means threading PMR allocators through
`String`, `DTree`, and every container.
2. **Fault recovery is best-effort, not sound.** The current
SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips
destructors, can resume over corrupted worker state, and (see
RECOMMENDATIONS.md 1.5) cannot reliably produce a useful backtrace.
A faulting unit *can* have scribbled on runtime memory before the signal.
3. **UCE can only ever host fully-trusted code.** A `.uce` page is arbitrary
native code with full process privileges. Multi-tenant or user-supplied
page hosting is structurally impossible in the native model.
A WASM execution model deletes the shared-fate fact itself rather than
patching around it:
- **Arena by construction:** each request runs in its own linear memory,
dropped wholesale at request end. Request-lifetime memory physically cannot
outlive the request; server-lifetime state physically cannot live inside
it. The `_scratchpad.cpp` design becomes correct because the boundary is
structural, not typological.
- **Sound recovery:** a trap (null deref, OOB, stack exhaustion) is a defined
host-side error that unwinds cleanly, with a precise guest stack trace, and
the unit cannot have touched host memory. Render error page, drop
workspace, keep serving — actually correct, not hopeful.
- **Capability security:** page code gets exactly the imported API surface
and nothing else. Multi-tenant hosting becomes possible.
- **Secondary wins:** architecture-independent unit artifacts (no clang
required on prod), safe module unload/replace (vs. never-safe `dlclose`),
first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU
timeout), per-request memory stats for free.
**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of
interpreted runtimes); a real ABI/membrane design; ownership of a custom
loader; toolchain rough edges (§10).
---
## 2. Rejected alternatives (recorded so they stay rejected)
- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and
threading allocators through the entire codebase; the failed global-swap
shortcut is the only cheap version and it is unsound (§1.1).
- **One instance per component.** UCE components are function calls, not
RPCs: callees receive the context **by reference**, mutate `context.call`,
share the `ob_*` capture stack and `ONCE` dedup state. Per-component
instances force serialize/copy/deserialize of the context on every
`component()` call (a dozen+ per page in the starter), require
host-mediation of the ob stack, and silently change reference semantics to
copy semantics. Rejected.
- **One linked module per app ("the blob").** Introduces an "app" concept
UCE does not have, makes every edit a global relink, turns the artifact
cache into a build graph — webpack's worst traits without its benefits.
Rejected. The file stays the unit.
- **Eager pre-loading of known units at worker warm-up.** Rejected; loading
is strictly lazy, on first explicit call, preserving current semantics.
---
## 3. Architecture overview
### 3.1 Execution model
```
host process (linux_fastcgi, per worker)
│
├─ vendored wasm runtime (§10.1)
├─ unit artifact cache: one PIC .wasm per unit (replaces per-unit .so)
├─ loader (§6): dylink parsing, base allocation, GOT resolution,
│ symbol registry, ABI stamp check
├─ core snapshot: "core module, initialized" memory+table image
│
└─ per request: WORKSPACE
├─ linear memory (CoW-born from core snapshot)
├─ shared funcref table
├─ core module instance (uce_lib + libc compiled to wasm)
├─ unit module instances (loaded lazily, on first call, incl. mid-request)
└─ host handle table (sqlite/mysql/file/socket handles + closers)
```
- **Workspace = request.** Born from the core snapshot, dropped at request
end. Memory drop is the arena; handle-table drop is resource cleanup
(this generalizes and replaces the per-connector
`cleanup_*_connections()` pattern — RECOMMENDATIONS.md 1.7 / 5.1 become
structurally unrepresentable).
- **One unit = one PIC wasm module.** Compile, cache, and invalidation
granularity stay per-file. The `.uce → C++` translation pipeline is
unchanged; only the compile target changes
(`clang --target=wasm32-wasi -fPIC` + `wasm-ld -shared`).
- **Strictly lazy loading.** A unit's module is instantiated into a
workspace the first time that workspace calls it — including mid-request.
This is the wasm equivalent of today's compile-and-`dlopen`-on-first-hit
and requires no restart, no fallback path. Placement memoization
(deterministic bases so repeat instantiation is cheaper) is a permitted
optimization; it must not change the loading policy.
- **In-flight isolation.** Module versions are immutable; a recompiled unit
becomes a new module. Running workspaces keep what they loaded; new
workspaces get the new version. Old modules are dropped when unreferenced
(safe unload — impossible with `dlclose`).
### 3.2 Memory model
- **One heap, one allocator, one DTree implementation** — all owned by the
core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT;
the loader **rejects any unit module that defines rather than imports
them** (two allocators on one heap is the one fatal misconfiguration).
- **Bump allocator option.** Because the workspace heap is dropped wholesale,
the core's allocator may be a bump allocator with no-op free — the
`_scratchpad.cpp` design, now correct by construction. Per-deployment
flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base
(replaces the tracking `operator new` in `types.h`).
- **Unit statics reset per request** (workspace is born from the core
snapshot, which does not include unit data; unit data segments initialize
at unit load within the workspace). This is *more* shared-nothing than
today, where `.so` statics persist across requests within a worker.
Cross-request state must use explicit host facilities (sessions, caches).
**Breaking change — must be called out in docs and checked against the
site/ tree during Phase 5.**
### 3.3 The host membrane
Exactly three currencies cross between host and workspace:
1. **scalars** (i32/i64/f64),
2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are
placed via the core's exported allocator),
3. **handles** (opaque `u32` indices into the per-workspace host handle
table; each entry carries a closer callback).
Everything pointer-shaped stays on its own side. Hostcall surface budget:
30–60 functions (§5.1). Host errors return as error values; traps are
reserved for unit faults. Nothing throws across the membrane.
**DTrees cross the membrane only as the versioned wire encoding** (§5.3),
at exactly three sites: request context in (once), response out (once),
bulk I/O results in (per query, optional vs. cursor-style hostcalls).
### 3.4 DTree inside the workspace: no serialization, ever
Within a workspace, all modules share one address space, one toolchain, one
set of headers — the C/C++ ABI is intact across module boundaries. Therefore:
- A `DTree` is a pointer (an i32 offset into linear memory).
- `component(path, context)` resolves path → table index (host registry or
guest-resident map) and `call_indirect`s, passing the context pointer.
Reference semantics, mutation visibility, shared ob stack, working `ONCE`
— identical to today.
- Function pointers are shared-table indices, valid across modules: virtual
calls, `std::function` callbacks (`dtree_map` lambdas) work across units.
- Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module
symbols — the same shape of overhead native PIC pays through the PLT/GOT,
i.e. what dlopened `.so` units pay today.
Encode/decode is **not** part of internal component calls. It exists only at
the membrane (§3.3) and on the cross-instance plane (§4).
### 3.5 The DTree C ABI (load-bearing, build it first)
The stable contract of the workspace is a **C ABI**, not the C++ class:
the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2),
plus the string/ob/print helpers. C++ units may bypass it and use the class
directly (same headers, zero cost — a private fast path). Every other
workspace language uses the C surface.
This ABI is versioned: every unit artifact carries a custom section
(`uce.abi`: core ABI version + toolchain fingerprint); the loader refuses
stale units and triggers lazy recompilation (units are lazily compiled
anyway, so this costs nothing structurally).
**Phase 1 of the implementation plan is to introduce this C ABI in the
current native runtime** — it is useful immediately (plugin surface,
testability) and de-risks the rest.
---
## 4. Two component-call planes / multi-language support
The design stratifies languages by one question: *can the toolchain produce a
PIC linear-memory module that adopts a foreign allocator?*
**Plane A — workspace peers** (C++, C, Rust, Zig, …):
- Join the workspace as PIC modules importing core symbols.
- Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator
parameter) and must not unwind across boundaries (`panic=abort` /
catch-at-edge).
- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale
calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` —
the borrow checker enforces the arena invariant).
**Plane B — runtime-carrying languages** (JS, Python, Go, C#, …):
- Their GC/runtime owns its memory; they run as **separate instances**
within the request and communicate through the host using the wire
encoding and handles. Bindings choose per-access hostcalls or bulk
subtree hydration into native dicts/objects — a tuning decision, not an
architectural fork.
**The semantic rule (enforced by the loader, not by convention):**
cross-plane components do **not** receive the mutable context. They get an
explicit interface — props in (copied by definition), rendered output and
declared results back. Plane A keeps the full "here's the world, mutate it"
contract. A `component()` call must never silently change mutation semantics
based on the callee's implementation language.
The cross-instance call mechanism is shared by: Plane B units, and future
cross-trust-boundary components (multi-tenant). Serialization boundaries and
isolation boundaries are the same lines, by design.
---
## 5. ABI sketches (to be finalized in Phase 0/1)
### 5.1 Hostcall surface (grouped; target ≤ 60 functions)
```
request: uce_host_ctx_read(buf) → len // wire-encoded context, once
response: uce_host_respond(status, hdrs_buf, body_buf)
uce_host_stream_write(buf) // chunked/streaming path
log: uce_host_log(level, buf)
sqlite: uce_host_sqlite_connect(path_buf) → handle | err
uce_host_sqlite_query(handle, sql_buf, params_buf) → result_buf | err
uce_host_sqlite_cursor_*(...) // optional row-cursor variant
uce_host_sqlite_insert_id/affected/error/disconnect(handle)
mysql: (same shape; existing connector APIs are already handle-shaped)
files: uce_host_file_read/write/stat/list(path_buf, ...) // policy-gated
session: uce_host_session_get/set(key_buf, val_buf)
http: uce_host_http_request(req_buf) → handle/result_buf // outbound
misc: uce_host_time(), uce_host_random(buf), uce_host_env(key_buf)
loader: uce_host_component_resolve(path_buf) → table_index // may load (§6)
ws: uce_host_ws_send(buf), event delivery via render entry re-invocation
```
Conventions: all errors as result codes + `uce_host_last_error(buf)`;
inbound buffers placed via the core's exported `uce_alloc`; no hostcall
traps on bad input (clamp/error instead).
### 5.2 DTree C ABI (core exports; sketch)
```c
typedef struct uce_dtree uce_dtree; // opaque; workspace-owned
uce_dtree* uce_dtree_root(void); // request context
uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write
uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent
const char* uce_dtree_value(uce_dtree*, size_t* len);
void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen);
size_t uce_dtree_count(uce_dtree*);
int uce_dtree_is_list(uce_dtree*);
/* iteration */
uce_dtree_iter uce_dtree_iter_begin(uce_dtree*);
int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*,
const char** key, size_t* klen, uce_dtree** child);
/* encode/decode at the membrane */
size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1
uce_dtree* uce_dtree_decode(const char* buf, size_t len);
/* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close,
uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */
```
No unwinding across this surface; C++ exceptions are caught at the edge and
surfaced as error returns where fallible.
### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only)
Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's
actual fields — value + ordered children):
```
node := value children
value := varint len, bytes (utf-8)
children := varint count, count × ( key: varint len + bytes, node )
flags := one leading byte per node reserved (bit0: is_list hint)
header := "UCEB" u8 version
```
This encoding is a **versioned protocol** from day one (header byte). It is
the Plane B contract and the membrane format; internal calls never see it.
### 5.4 Unit module contract
```
custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id }
imports: env.memory, env.__indirect_function_table,
env.__memory_base, env.__table_base,
GOT.mem.* / GOT.func.* (resolved by loader),
core symbols (malloc, uce_dtree_*, uce_print, ...)
exports: uce_unit_setup, uce_unit_render,
uce_unit_component, uce_unit_websocket
(same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET
dlsym symbols in compiler.cpp)
forbidden: defining malloc/free/operator new, own memory, start fn
with side effects beyond data init
```
---
## 6. The loader (host-side, custom, load-bearing)
Owned code, ~1–2k lines, vendored-runtime-adjacent. Reference logic:
Emscripten's dylink loader (the ABI is the stable, battle-tested part; the
server-side loader is what doesn't exist off the shelf).
Per `load(unit)` into a workspace:
1. Fetch compiled module from artifact cache (compile on miss — today's
lazy-compile path, retargeted).
2. Verify `uce.abi` stamp against the core; on mismatch, recompile unit.
3. Verify import discipline (no allocator/runtime definitions; §3.2).
4. Parse `dylink.0`: data size/alignment, table slots needed.
5. Allocate `__memory_base` (bump within workspace data region) and
`__table_base` (append to shared table).
6. Instantiate with bases; resolve `GOT.*` imports against the workspace
symbol registry (core symbols + previously loaded units); register the
unit's exports.
7. Register entry points in the path → table-index dispatch map.
`uce_host_component_resolve(path)` consults the dispatch map and calls
`load()` on miss — this is how lazy, programmatic, mid-request loading works
with no special cases.
Placement memoization (optional, later): record each unit's first-assigned
bases; reuse across workspaces so instantiation is cheaper and snapshot
growth (below) stays consistent. Does not change the lazy policy.
**Core snapshot:** the only pre-built state is "core module, initialized" —
memory bytes + table state captured once per core build. Workspaces are born
from it via CoW (`mmap(MAP_PRIVATE)` of the snapshot image; the host owns
the Memory object, so OS-level CoW is available). No units are pre-fed.
---
## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp)
```
1. accept request
2. workspace = birth_from_core_snapshot() (CoW, ~µs)
3. write wire-encoded context into workspace; core decodes → context DTree
4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr)
5. component(path) inside guest → resolve hostcall → (lazy load) →
call_indirect — reference semantics throughout
6. I/O via hostcalls; resources land in the workspace handle table
7. on return: encode response/headers out; write FastCGI response
on trap: defined error → render error page with guest stack trace;
workspace state is irrelevant because…
8. drop workspace: linear memory gone (arena), handle table closed
(generalized resource cleanup), instances released
```
CPU limit: epoch/fuel interruption → same path as trap.
Memory limit: linear memory max → allocation failure / trap → same path.
---
## 8. What carries over unchanged
- The `.uce → C++` translation, parser, and page semantics.
- The lazy compile-on-first-request model and per-unit artifact caching
(different artifact format).
- The host-side connectors (sqlite/mysql) — already handle-shaped APIs; they
move behind hostcalls with the same `.uce`-visible signatures.
- The site tree, docs, demo, and the network test suite
(`tests/run_network_tests.py`) — which becomes the parity harness (§9).
- nginx/FastCGI front-end integration, worker model, websocket event flow
(events re-enter via the websocket entry point; note: a workspace-per-event
or workspace-per-connection decision is an open question, §11).
---
## 9. Implementation plan
Phases are sequential; each has an exit criterion. No phase except 0 and 2's
scaffolding produces throwaway work.
**Phase 0 — toolchain & runtime spike (timeboxed).**
Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated
unit; cross-module C++ calls with shared memory/table; exceptions decision
(wasm EH vs. error-code discipline at unit boundaries — pick one, record it);
vendored runtime selection. Candidates: **WAMR** (C, small, designed for
embedding, easiest to vendor and patch — fits the project's vendoring
practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier
to vendor/patch). Selection criteria: imported-memory + shared-table support,
AOT artifact quality, patchability. Exit: a two-module (core stub + unit
stub) hello-world linked at runtime by a minimal loader, in the chosen
vendored runtime.
**Phase 1 — DTree C ABI in the native runtime.**
Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used
natively. Zero wasm dependency; immediately testable; freezes the contract
everything else builds on. Exit: codec round-trip + accessor tests in the
existing suite; ABI doc checked in.
**Phase 2 — core module + membrane.**
Compile `uce_lib` (+ wasi-libc) to wasm as the core module; implement the
hostcall surface (§5.1) in the host; temporary scaffolding allowed: one
statically-linked unit + core to validate codegen and membrane without the
loader. Exit: one real `.uce` page (e.g. `site/tests/core.uce`) renders
correctly through the membrane. Scaffolding is marked throwaway.
**Phase 3 — the loader + workspace.**
Implement §6 in full: dylink parsing, base allocation, GOT resolution,
ABI/import verification, lazy mid-request loading, path dispatch. Per-request
workspace birth/drop (plain memcpy birth is fine here; CoW is Phase 4).
Exit: the uce-starter renders end-to-end with components loading lazily;
`tests/run_network_tests.py --match starter` passes against the wasm worker.
**Phase 4 — production mechanics.**
Core snapshot + CoW birth; bump-allocator flag; epoch/memory limits; trap →
error-page path with guest stack traces (this supersedes the
signal/longjmp machinery and closes RECOMMENDATIONS.md 1.5 structurally);
handle-table cleanup (closes 1.7/5.1); artifact/ABI versioning end-to-end.
Exit: kill-tests (deliberate null-deref page, infinite-loop page, OOM page)
produce clean error pages and an unharmed worker.
**Phase 5 — parity & performance.**
Full network suite green on the wasm worker; differential native-vs-wasm runs
on the site tree; audit `site/` for cross-request-static reliance (§3.2
breaking change); benchmark suite (template-heavy page, sqlite page,
component-heavy starter page) with budgets: ≤2× native page latency,
workspace birth ≤100µs, internal component call overhead within 10× native
call cost. Exit: numbers published in this document; go/no-go for default
backend.
**Phase 6 — second plane (deferred until wanted).**
Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B
language binding, loader enforcement of the cross-plane context rule (§4).
Plane A second language (Rust) as the cheaper first polyglot proof.
The native `.so` backend remains in-tree and selectable until Phase 5's
go/no-go; both backends share the Phase 1 C ABI.
---
## 10. Risks & mitigations
| Risk | Mitigation |
|---|---|
| wasi-sdk PIC / shared-library maturity (least-trodden toolchain path) | Phase 0 spike before any commitment; pin toolchain versions; statically link libc into the core and export from there (avoid shared wasi-libc entirely) |
| C++ exceptions × PIC × wasm EH | Phase 0 decision point; fallback is error-code discipline at unit entry points (units already have a uniform entry shape) |
| Custom loader correctness (GOT, bases, relocation) | Small, contained (~1–2k lines); crib logic from Emscripten's reference loader; fuzz with adversarial modules; loader rejects > loader guesses |
| Vendored runtime patches drift from upstream | Same practice as vendored SQLite: provenance + patch files under `docs/patches/`; pin upstream tag; tests gate upgrades |
| Performance regression beyond budget | Phase 5 gates; bump allocator and placement memoization in reserve; native backend retained |
| Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging |
| Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces |
## 11. Open questions
1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.)
2. **WebSocket granularity:** workspace per event (pure arena, statics reset
per event) or per connection (state across events, bounded lifetime)?
Leaning per-event for consistency; needs a look at current WS page usage.
3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list
flag) — finalize in Phase 1.
4. **Cursor vs. bulk** as the default for `sqlite_query` results at the
membrane (offer both; pick the default after Phase 5 benchmarks).
5. **Streaming output:** today's ob model buffers; does the membrane expose
`uce_host_stream_write` from day one or post-MVP?
6. **Core snapshot rebuild cadence** once placement memoization lands
(dead-base reclamation policy).
## 12. Summary
The module is the **unit** (file-grained, lazily compiled, lazily loaded —
including mid-request). The instance set is the **workspace** (one per
request; shared memory + table; born from a core-only CoW snapshot; dropped
wholesale — the arena, done right). The contract is the **DTree C ABI**
inside the workspace (pointer semantics, no serialization) and the **UCEB1
wire encoding + handles** at every true address-space boundary (host
membrane, Plane B languages, future trust boundaries). Serialization
boundaries and isolation boundaries are the same lines; component calls stay
function calls; and the file stays the unit.
@@ -0,0 +1,12 @@
diff --git a/vendor/miniz/miniz_tdef.c b/vendor/miniz/miniz_tdef.c
--- a/vendor/miniz/miniz_tdef.c
+++ b/vendor/miniz/miniz_tdef.c
@@
-static const mz_uint s_tdefl_num_probes[11];
+static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
static int tdefl_flush_block(tdefl_compressor *d, int flush)
@@
-static const mz_uint s_tdefl_num_probes[11] = { 0, 1, 6, 32, 16, 32, 128, 256, 512, 768, 1500 };
-
/* level may actually range from [0,10] (10 is a "hidden" max level, where we want a bit more compression and it's fine if throughput to fall off a cliff on some files). */
+34
View File
@@ -0,0 +1,34 @@
# SQLite 3.46.1 Vendor Import
Imported the official SQLite amalgamation without local source modifications.
- Version: SQLite 3.46.1
- Upstream archive: https://www.sqlite.org/2024/sqlite-amalgamation-3460100.zip
- Import date: 2026-05-29
- Vendored files:
- `src/3rdparty/sqlite/sqlite3.c`
- `src/3rdparty/sqlite/sqlite3.h`
- `src/3rdparty/sqlite/sqlite3ext.h`
## Checksums
```text
77823cb110929c2bcb0f5d48e4833b5c59a8a6e40cdea3936b99e199dbbe5784 sqlite-amalgamation-3460100.zip
6c35bc5f7f85eac9c49928bacbb02bb694b547aabf69197e058cca245ad80e83 sqlite3.c
89b62c671c5964e137409ce034941b7b05a3af2c9875aba41f47f9483d0c2515 sqlite3.h
b184dd1586d935133d37ad76fa353faf0a1021ff2fdedeedcc3498fff74bbb94 sqlite3ext.h
```
## Runtime compile flags
The amalgamation is included by `src/lib/uce_lib.cpp` with:
```cpp
#define SQLITE_THREADSAFE 1
#define SQLITE_OMIT_LOAD_EXTENSION 1
#define SQLITE_DQS 0
#define SQLITE_DEFAULT_FOREIGN_KEYS 1
#define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1
```
No patch diff is needed because the vendored SQLite files are unmodified.
+147
View File
@@ -0,0 +1,147 @@
# React Developer Affordances Todo
## Objective
Add practical value for developers coming from React frameworks while preserving UCE's server-first C++ model. Defer component syntax/children work, avoid global head/assets/islands in the runtime, and focus on function-library data helpers, diagnostics, docs, examples, demos, and a starter-local router with starter-local asset/island components.
## Success Criteria
- [x] Function library has useful collection/data-shaping helpers with docs and tests.
- [x] Compile/runtime diagnostics are more helpful, especially for generated-code and common preprocessor mistakes.
- [x] Docs include a concise React/Next/Remix orientation guide.
- [x] Starter example uses a centralized hierarchical/file-based router in `index.uce` efficiently.
- [x] Starter-local asset/island affordances live as component handlers in the starter, not global runtime APIs.
- [x] Network tests and relevant build checks pass on `k-uce`.
## Current State
- Status: complete
- Last updated: 2026-05-28
- Source of truth: `/root/mount_ssh/k-uce-root-htdocs-uce`
- Runtime/live target: `k-uce:/Code/uce.openfu.com/uce`; rebuilt and restarted `uce.service` on `k-uce`.
## Goal Tree
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
- [x] G1: Add collection/data helpers to function library
- Why: React-framework developers routinely shape arrays/objects near render code.
- Done when: helpers are declared, implemented, documented, and covered by tests.
- Verify: build plus focused site/network tests.
- [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface.
- [x] G1.2: Implement minimal high-value helpers without broad template complexity.
- [x] G1.3: Add docs and examples for helpers.
- [x] G1.4: Add/extend tests.
- [x] G2: Improve developer diagnostics
- Why: React frameworks win by making failures easy to act on.
- Done when: compile/runtime error output includes actionable context and docs mention debugging flow.
- Verify: intentional broken page surfaces improved message.
- [x] G2.1: Inspect current compiler/runtime error rendering path.
- [x] G2.2: Add source excerpt / generated path / common-hint text where appropriate.
- [x] G2.3: Document diagnostics.
- [x] G3: Add React/Next/Remix orientation docs
- Why: mapping familiar concepts reduces onboarding cost without adding syntax.
- Done when: docs page exists and is linked from docs/README/demo surfaces.
- Verify: docs page renders live.
- [x] G4: Starter-local router and starter affordances
- Why: User specifically wants hierarchical/file-based routing beautifully in starter `index.uce`.
- Done when: starter routes go through a central router in `index.uce`, and starter-local asset/island component handlers exist and are used where sensible.
- Verify: key starter routes render 200.
- [x] G4.1: Inspect current starter routing.
- [x] G4.2: Refactor to clear route table / hierarchical file resolution in `index.uce`.
- [x] G4.3: Add starter-local `COMPONENT:asset` / `COMPONENT:island` style handlers in one unit.
- [x] G4.4: Use them efficiently in starter pages/layout.
- [x] G5: Demos and examples
- Why: Affordances must be visible to developers, not hidden in APIs.
- Done when: docs/demo/tests expose examples.
- Verify: demo URLs return 200 and tests pass.
- [x] G6: Verification and project docs
- Done when: build/test commands are run on `k-uce`, project notes updated, and adversarial review completed.
## Execution Queue
Complete.
## Decisions
- 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax.
- 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components.
- 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`.
- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`.
## Assumptions
- Current source-of-truth mount is live-editable; runtime validation requires SSH to `k-uce`.
- Existing site tests are the right place for function-library coverage.
## Blockers and Risks
- No current blockers.
- Future risk: if UCE grows a full parser or component-tag syntax, keep this pass's explicit component/router APIs as a stable lower-level fallback.
## Evidence and Verification Log
- 2026-05-28: Created plan after reviewing README, preprocessor docs, and function library headers.
- 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded.
- 2026-05-28: Restarted `uce.service` on `k-uce`.
- 2026-05-28: `tests/run_network_tests.py --match core` passed.
- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`.
- 2026-05-28: Full internal network suite passed, 25/25.
- 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward.
## Change Log
- 2026-05-28: Created initial goal tree.
- 2026-05-28: Implemented helpers, diagnostics, docs, demo, starter router, starter-local web affordance components, tests, and validation.
## Follow-up Cleanup 2026-05-29
- Removed duplicate route cleanup from `starter_router_candidates()` because `app_make_route()` is the single normalization point for `l_path`.
- Weeded out nearby duplicate/obsolete starter code:
- `starter_router_add_candidate(...)` now owns repeated candidate tree construction.
- Removed unused `app_resolve_view()` / `starter_resolve_view()` after moving routing into starter `index.uce`.
- Removed unused legacy registered asset rendering functions from `lib/app.uce`; registered assets now render through `components/theme/web_affordances.uce`.
- `app_init()` now reuses `app_base_url(context)` instead of repeating base URL derivation.
- `web_affordances.uce` now uses one `starter_render_asset_group(...)` loop for CSS and JS.
- Verification: rebuilt on `k-uce`, restarted `uce.service`, checked key starter routes, and ran `tests/run_network_tests.py --match core` successfully.
## Follow-up Routed Views 2026-05-29
- Changed starter route dispatch from `unit_render(...)` to `component(...)`.
- Converted all routed `site/examples/uce-starter/views/*.uce` files to `COMPONENT(Request& context)` rather than `RENDER(Request& context)` because they are central-router-only views.
- Updated the starter README and verified key starter routes. No service restart was required because only `.uce`/docs changed.
## Follow-up Canonical Starter URLs 2026-05-29
- Canonicalized starter self-links from `/examples/uce-starter/index.uce?...` to `/examples/uce-starter/?...` with `app_canonical_script_url(...)`.
- Updated the starter README to show canonical directory URLs.
- Touched the starter front controller to force the `#load`ed helper change into the cached generated unit.
- Verified canonical/direct starter routes and checked generated self-links. No service restart was required.
## Follow-up Not Found Component 2026-05-29
- Moved `starter_router_render_not_found` markup into `components/basic/notfound.uce`.
- Router now delegates 404 body rendering through `component("components/basic/notfound", props, context)`.
- Verified missing routes return `404` and normal dashboard route returns `200`. No service restart required.
## Follow-up Page Shell Component 2026-05-29
- Moved app page rendering into `themes/page.uce` as a component.
- Removed `app_render_page`, `app_theme_page_component`, and `starter_render_page` from `lib/app.uce`.
- Page template resolution now follows `context.call["app"]["page_type"]`: current theme first, common fallback second.
- Verified representative HTML routes and the JSON page-type fallback. No service restart required.
## Follow-up Deep Starter Context Cleanup 2026-05-29
- Removed repeated `starter_boot(context)` calls; root `index.uce` is the boot point.
- Removed `context.call["starter"]` duplicated state and JSON side-channel state.
- JSON routes now use only `context.call["app"]["page_type"]` plus normal captured output.
- Simplified `themes/page.uce` and `themes/common/page.json.uce` accordingly.
- Replaced `starter_*` alias helper usage with direct `app_*` helpers and removed alias wrappers except `StarterUser`.
- Verified key routes and core tests. No service restart required.
## Follow-up Route Context Flattening 2026-05-29
- Flattened `context.call["app"]["route"]` to `context.call["route"]`.
- Moved former `context.call["app"]["router"]` metadata into `context.call["route"]`.
- Verified representative starter HTML, 404, and JSON routes. No service restart required.
+34 -2
View File
@@ -7,6 +7,10 @@ SESSION_PATH=/var/lib/uce/sessions
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
FCGI_PORT=9993
# HTTP-over-Unix command socket for local CLI/control tooling.
# Example: curl --unix-socket /run/uce/cli.sock http://localhost/ping
CLI_SOCKET_PATH=/run/uce/cli.sock
# OPTIONAL PROACTIVE COMPILE ROOT
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
PRECOMPILE_FILES_IN=
@@ -20,8 +24,8 @@ JIT_COMPILE_ON_REQUEST=1
# ENABLE THE BACKGROUND PROACTIVE COMPILER LOOP
PROACTIVE_COMPILE_ENABLED=1
# AFTER A FAILED COMPILE, SERVE THE LAST COMPILER OUTPUT AND WAIT THIS MANY SECONDS
# BEFORE TRYING AGAIN IF THE SOURCE FILE HAS NOT CHANGED
# AFTER A FAILED COMPILE, UCE NOW REUSES THE PERSISTED COMPILER OUTPUT UNTIL
# THE SOURCE OR COMPILER INPUTS CHANGE. THIS SETTING IS KEPT FOR COMPATIBILITY.
COMPILE_FAILURE_RETRY_SECONDS=10
# PERIODIC KNOWN-.uce RECHECK INTERVAL IN SECONDS
@@ -33,5 +37,33 @@ WORKER_COUNT=4
# MAX MEMORY PER REQUEST
MAX_MEMORY=16777216
# TRANSPORT / DOS LIMITS
TRANSPORT_MAX_CLIENT_CONNECTIONS=256
TRANSPORT_MAX_HTTP_HEADER_BYTES=16384
TRANSPORT_MAX_HTTP_BODY_BYTES=1048576
TRANSPORT_MAX_WEBSOCKET_FRAME_BYTES=1048576
TRANSPORT_MAX_WEBSOCKET_MESSAGE_BYTES=1048576
TRANSPORT_MAX_WEBSOCKET_OUTPUT_BYTES=4194304
TRANSPORT_MAX_RESPONSE_BYTES=8388608
TRANSPORT_HTTP_REQUEST_TIMEOUT_SECONDS=15
TRANSPORT_CONNECTION_IDLE_TIMEOUT_SECONDS=120
# Empty means COMPILER_SYS_PATH for the built-in direct HTTP listener.
HTTP_DOCUMENT_ROOT=
# CUSTOM SERVER LIMITS
CUSTOM_SERVER_MAX_SERVERS=16
CUSTOM_SERVER_MIN_PORT=1024
CUSTOM_SERVER_MAX_PORT=65535
CUSTOM_SERVER_ALLOW_PUBLIC_BIND=0
CUSTOM_SERVER_UNIX_SOCKET_PREFIX=/tmp/uce/custom-servers/
CUSTOM_SERVER_HANDLER_TIMEOUT_SECONDS=30
# Empty means COMPILER_SYS_PATH/SITE_DIRECTORY.
CUSTOM_SERVER_UCE_ROOT=
# ARCHIVE HELPER LIMITS
ARCHIVE_MAX_INPUT_BYTES=67108864
ARCHIVE_MAX_OUTPUT_BYTES=67108864
ARCHIVE_MAX_ZIP_ENTRIES=4096
# LIFETIME OF SESSION COOKIES IN SECONDS
SESSION_TIME=2592000
+14 -3
View File
@@ -14,11 +14,22 @@ mkdir work > /dev/null 2>&1
COMPILER="clang++"
FLAGS="-g -rdynamic -w -Wall -$OPT_FLAG -std=c++20 -fpermissive -ffast-math"
LIBS="-ldl -lm -lpthread `mysql_config --cflags --libs`"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs`"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
echo "Compliling executable..."
time -p $COMPILER src/linux_fastcgi.cpp $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
echo "Compiling SQLite..."
clang -g -O2 -fPIC \
-DSQLITE_THREADSAFE=1 \
-DSQLITE_OMIT_LOAD_EXTENSION=1 \
-DSQLITE_DQS=0 \
-DSQLITE_DEFAULT_FOREIGN_KEYS=1 \
-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \
-c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1
if [ $? -ne 0 ]; then exit 1; fi
echo "Compiling executable..."
time -p $COMPILER src/linux_fastcgi.cpp bin/sqlite3.o $SRCFLAGS $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1
if [ $? -eq 0 ]
then
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -euo pipefail
default_socket="/run/uce/cli.sock"
if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
configured_socket=$(awk -F= '/^[[:space:]]*CLI_SOCKET_PATH[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_socket:-}" ]]; then
default_socket="$configured_socket"
fi
fi
socket_path="${UCE_CLI_SOCKET:-$default_socket}"
method="auto"
raw_json=""
usage() {
cat <<'USAGE'
Usage:
scripts/uce-cli [options] <unit-or-hook> [key=value ...]
Options:
--socket PATH Unix socket path (default: CLI_SOCKET_PATH from /etc/uce/settings.cfg,
UCE_CLI_SOCKET, or /run/uce/cli.sock)
--get Send key=value parameters as a query string
--post Send key=value parameters as a JSON POST (default when params exist)
--json JSON Send raw JSON as POST body
-h, --help Show this help
Examples:
scripts/uce-cli /ping
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
scripts/uce-cli --get /tests/cli.uce action=echo message=hello
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--socket)
[[ $# -ge 2 ]] || { echo "--socket requires a path" >&2; exit 2; }
socket_path="$2"
shift 2
;;
--get)
method="get"
shift
;;
--post)
method="post"
shift
;;
--json)
[[ $# -ge 2 ]] || { echo "--json requires a JSON value" >&2; exit 2; }
raw_json="$2"
method="post"
shift 2
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "unknown option: $1" >&2
usage >&2
exit 2
;;
*)
break
;;
esac
done
[[ $# -ge 1 ]] || { usage >&2; exit 2; }
path="$1"
shift
if [[ "$path" != /* ]]; then
path="/$path"
fi
if [[ ! -S "$socket_path" ]]; then
echo "UCE CLI socket not found: $socket_path" >&2
exit 1
fi
if [[ "$method" == "auto" ]]; then
if [[ $# -gt 0 || -n "$raw_json" ]]; then
method="post"
else
method="get"
fi
fi
if [[ -n "$raw_json" ]]; then
printf '%s' "$raw_json" | python3 -m json.tool >/dev/null
curl -sS --fail-with-body --unix-socket "$socket_path" \
-X POST \
-H 'Content-Type: application/json' \
--data-binary "$raw_json" \
"http://localhost$path"
exit $?
fi
if [[ "$method" == "get" ]]; then
url="http://localhost$path"
if [[ $# -gt 0 ]]; then
query=$(python3 - "$@" <<'PY'
import sys, urllib.parse
pairs = []
for arg in sys.argv[1:]:
if '=' not in arg:
raise SystemExit(f"argument is not key=value: {arg}")
key, value = arg.split('=', 1)
pairs.append((key, value))
print(urllib.parse.urlencode(pairs))
PY
)
url="$url?$query"
fi
curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
else
json=$(python3 - "$@" <<'PY'
import json, sys
payload = {}
for arg in sys.argv[1:]:
if '=' not in arg:
raise SystemExit(f"argument is not key=value: {arg}")
key, value = arg.split('=', 1)
lowered = value.lower()
if lowered == 'true':
parsed = True
elif lowered == 'false':
parsed = False
elif lowered in ('null', 'none'):
parsed = None
else:
try:
parsed = int(value)
except ValueError:
try:
parsed = float(value)
except ValueError:
parsed = value
payload[key] = parsed
print(json.dumps(payload, separators=(',', ':')))
PY
)
curl -sS --fail-with-body --unix-socket "$socket_path" \
-X POST \
-H 'Content-Type: application/json' \
--data-binary "$json" \
"http://localhost$path"
fi
+41
View File
@@ -0,0 +1,41 @@
#include "demo_guard.h"
RENDER(Request& context)
{
DTree p;
p.set(context.params);
StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"};
StringList unique_routes = list_unique(routes);
StringList sorted_routes = list_sort(unique_routes);
StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); });
DTree cards;
DTree card;
card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear();
card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear();
card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card);
DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); });
<><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
</head>
<body>
<h1><a href="index.uce">UCE Test Suite</a><a class="docs-link" href="../doc/index.uce?p=map">Collection Docs &rarr;</a></h1>
<h2>Collection Helpers</h2>
<p>Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.</p>
<div class="system-info"><h3>StringList route labels</h3><pre><?= join(labels, "\n") ?></pre></div>
<div class="system-info"><h3>DTree app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(p) ?></pre>
</details>
</body>
</html></>
}
@@ -3,7 +3,7 @@ RENDER(Request& context)
{
DTree card_props;
card_props["title"] = "Component Example";
card_props["body"] = "This card body comes from context.call and is rendered through component().";
card_props["body"] = "This card body comes from context.props and is rendered through component().";
DTree named_props;
named_props["title"] = "Named Render Example";
+24
View File
@@ -0,0 +1,24 @@
COMPONENT(Request& context)
{
<>
<section style="border:1px solid #ccc;padding:1em;margin:1em 0;">
<? component_render(":TITLE", context.props, context); ?>
<? component_render(":BODY", context.props, context); ?>
</section>
</>
}
COMPONENT:TITLE(Request& context)
{
<>
<h3><?= first(context.props["title"].to_string(), "Component Title") ?></h3>
</>
}
COMPONENT:BODY(Request& context)
{
<>
<p><?= first(context.props["body"].to_string(), "Component Body") ?></p>
</>
}
@@ -0,0 +1,10 @@
COMPONENT(Request& context)
{
String lang = first(context.props["lang"].to_string(), "plain");
<>
<section>
<p><strong>Code block:</strong> <?= lang ?></p>
<div><?: context.props["default_html"].to_string() ?></div>
</section>
</>
}
+15
View File
@@ -0,0 +1,15 @@
COMPONENT(Request& context)
{
String title = first(
context.props["node"]["attrs"]["title"].to_string(),
context.props["argument"].to_string(),
"Notice"
);
<>
<aside>
<p><strong><?= title ?></strong></p>
<div><?: context.props["children_html"].to_string() ?></div>
</aside>
</>
}
@@ -27,6 +27,7 @@ RENDER(Request& context)
<a class="docs-link" href="../doc/index.uce">API Docs &rarr;</a>
</h1>
<div class="test-grid">
<div class="grid-heading">Basics</div>
<? render_card("hello.uce", "Hello World", "Basic output and server time"); ?>
<? render_card("header.uce", "Headers", "HTTP response headers"); ?>
@@ -36,8 +37,12 @@ RENDER(Request& context)
<div class="grid-heading">Data Types &amp; Parsing</div>
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DTree"); ?>
<? render_card("yaml.uce", "YAML", "Concise config files with DTree"); ?>
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
<? render_card("string.uce", "String", "String operations"); ?>
<? render_card("collections.uce", "Collection Helpers", "Map, filter, group, pick, and omit render data"); ?>
<? render_card("str_replace.uce", "String Replace", "Search and replace in strings"); ?>
<? render_card("utf8.uce", "UTF-8", "Unicode string handling"); ?>
<? render_card("random.uce", "RNG / Noise", "Random generation and noise"); ?>
@@ -52,14 +57,17 @@ RENDER(Request& context)
<div class="grid-heading">Storage &amp; I/O</div>
<? render_card("fileio.uce", "File I/O", "Read and write files"); ?>
<? if(allow_server_demos) { render_card("zip.uce", "ZIP", "Create, list, read, and extract ZIP archives"); } ?>
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
<? if(allow_server_demos) { render_card("sqlite.uce", "SQLite", "Embedded SQLite database connector"); } ?>
<? if(allow_server_demos) { render_card("mysql.uce", "MySQL", "MySQL database connector"); } ?>
<div class="grid-heading">Advanced</div>
<? render_card("call_file.uce", "unit_call()", "Dynamic file inclusion"); ?>
<? render_card("components.uce", "Components", "Reusable component system"); ?>
<? render_card("once-init.uce", "ONCE / INIT", "Unit lifecycle hooks for worker load and request entry"); ?>
<? render_card("markdown.uce", "Markdown", "Markdown parsing with components"); ?>
<? render_card("script.uce", "Script", "UCE script integration"); ?>
<? render_card("websockets.ws.uce", "WebSockets", "Real-time WebSocket chat"); ?>
+53
View File
@@ -0,0 +1,53 @@
static s64 demo_worker_init_count = 0;
static s64 demo_component_hits = 0;
INIT(Request& context)
{
(void)context;
demo_worker_init_count += 1;
}
ONCE(Request& context)
{
context.call["once_hits"] = context.call["once_hits"].to_s64() + 1;
}
COMPONENT:PROBE(Request& context)
{
demo_component_hits += 1;
<>
<div class="banner">
<strong><?= context.props["label"].to_string() ?></strong>
<div>worker INIT count for this loaded unit: <?= (u64)demo_worker_init_count ?></div>
<div>request ONCE count for this request: <?= context.call["once_hits"].to_u64() ?></div>
<div>component handler calls served by this worker copy: <?= (u64)demo_component_hits ?></div>
</div>
</>
}
RENDER(Request& context)
{
DTree first;
first["label"] = "First component call";
DTree second;
second["label"] = "Second component call in the same request";
DTree third;
third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")";
<><html>
<link rel="stylesheet" href='style.css?v=<?= time() ?>'></link>
<h1>
<a href="index.uce">UCE Test</a>:
ONCE() and INIT()
</h1>
<p>
This page calls the same named component twice. `ONCE()` should only run once for the request, while `INIT()` should stay stable for the currently loaded worker copy.
</p>
<?: component(":PROBE", first, context) ?>
<?: component(":PROBE", second, context) ?>
<? unit_call("once-init.uce", "COMPONENT:PROBE", &third); ?>
</html></>
}
@@ -11,6 +11,7 @@ RENDER(Request& context)
</h1>
<p>This page exists to prove the template parser ignores quotes and template markers that appear inside C++ comments.</p>
<p>It also renders a literal raw-string terminator sequence safely: <code>)"</code>.</p>
<?
// Regression: this comment's apostrophe must not swallow the later ?> marker.
@@ -36,4 +37,4 @@ RENDER(Request& context)
<pre><?= var_dump(context.params) ?></pre>
</details>
</>
}
}
+74
View File
@@ -0,0 +1,74 @@
#include "demo_guard.h"
void regex_row(String label, String result, String expect)
{
bool pass = (result == expect);
<><tr>
<td class="test-label"><?= label ?></td>
<td class="test-result"><code><?= result ?></code></td>
<td class="test-status <?= pass ? "pass" : "fail" ?>"><?= pass ? "pass" : "expected: " + expect ?></td>
</tr></>
}
void regex_bool_row(String label, bool result, bool expect)
{
regex_row(label, result ? "true" : "false", expect ? "true" : "false");
}
RENDER(Request& context)
{
String text = "Contact ops@example.test or tag #uce, #docs, and café.";
DTree first_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", text);
DTree tags = regex_search_all("#(?<tag>[A-Za-z0-9_]+)", text);
StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown");
<>
<link rel="stylesheet" href='style.css'></link>
<style>
table.tests { width: 100%; border-collapse: collapse; margin-bottom: 8px; }
table.tests td { padding: 6px 12px; border-bottom: 1px solid rgba(255,255,255,0.06); font-family: var(--font-mono); font-size: 0.88rem; }
.test-label { color: var(--text-dim); white-space: nowrap; width: 1%; }
.test-result code { background: var(--bg-code); padding: 2px 8px; border-radius: 4px; }
.test-status.pass { color: #6f6; width: 1%; }
.test-status.fail { color: #f66; }
</style>
<h1>
<a href="index.uce">UCE Test</a>:
Regular Expressions
</h1>
<p>UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.</p>
<p>sample = <code><?= text ?></code></p>
<h2>Validation And Search</h2>
<table class="tests"><?
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice\")", regex_match("[A-Z][a-z]+", "Alice"), true);
regex_bool_row("regex_match(\"[A-Z][a-z]+\", \"Alice!\")", regex_match("[A-Z][a-z]+", "Alice!"), false);
regex_bool_row("regex_search(\"example\", text)[\"matched\"]", first_email["matched"].to_bool(), true);
regex_row("first_email[\"match\"]", first_email["match"].to_string(), "ops@example.test");
regex_row("first_email[\"named\"][\"user\"]", first_email["named"]["user"].to_string(), "ops");
regex_row("first_email[\"named\"][\"host\"]", first_email["named"]["host"].to_string(), "example.test");
regex_bool_row("regex_match(\"\\\\p{L}+\", \"café\")", regex_match("\\p{L}+", "café"), true);
?></table>
<h2>All Matches</h2>
<table class="tests"><?
regex_row("regex_search_all hashtags count", tags["count"].to_string(), "2.000000");
regex_row("first hashtag", tags["matches"]["0"]["named"]["tag"].to_string(), "uce");
regex_row("second hashtag", tags["matches"]["1"]["named"]["tag"].to_string(), "docs");
?></table>
<h2>Replace And Split</h2>
<table class="tests"><?
regex_row("regex_replace(\"#([A-Za-z0-9_]+)\", \"<tag>$1</tag>\", \"#uce\")", regex_replace("#([A-Za-z0-9_]+)", "<tag>$1</tag>", "#uce"), "<tag>uce</tag>");
regex_row("join(regex_split(\"\\\\s*,\\\\s*\", ...), \"|\")", join(pieces, "|"), "uce|components|markdown");
regex_row("case-insensitive flag", regex_search("uce", "Hello UCE", "i")["match"].to_string(), "UCE");
?></table>
<h2>Structured Result</h2>
<pre><?= json_encode(first_email) ?></pre>
<p><a href="../doc/index.uce?p=regex_search">Read the regex API documentation</a></p>
</>
}
+54
View File
@@ -0,0 +1,54 @@
#include "demo_guard.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "SQLite demo", "write to a server-side SQLite database");
return;
}
String db_path = "/tmp/uce-demo-sqlite.sqlite";
SQLite* db = sqlite_connect(db_path);
sqlite_query(db, "create table if not exists notes(id integer primary key autoincrement, body text not null, created_at text not null)");
if(context.params["REQUEST_METHOD"] == "POST" && trim(context.post["body"]) != "")
{
StringMap params;
params["body"] = trim(context.post["body"]);
params["created_at"] = time_format_utc("%Y-%m-%d %H:%M:%S");
sqlite_query(db, "insert into notes(body, created_at) values(:body, :created_at)", params);
sqlite_disconnect(db);
redirect("sqlite.uce", 303);
return;
}
DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10");
String error = sqlite_error(db);
?><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
</head>
<body>
<h1><a href="index.uce">UCE Demo</a> / SQLite</h1>
<div class="system-info">
<p>This demo stores rows in <code><?= db_path ?></code> with runtime SQLite helpers and named prepared parameters.</p>
<form method="post">
<input name="body" placeholder="note text"></input>
<button type="submit">Add note</button>
</form>
<? if(error != "ok" && error != "connected") { ?><pre><?= error ?></pre><? } ?>
</div>
<div class="test-grid">
<? notes.each([&](DTree note, String key) { ?>
<div class="test-card">
<strong>#<?= note["id"].to_string() ?> <?= note["created_at"].to_string() ?></strong>
<span><?= note["body"].to_string() ?></span>
</div>
<? }); ?>
</div>
</body>
</html><?
sqlite_disconnect(db);
}
+1 -1
View File
@@ -68,7 +68,7 @@ RENDER(Request& context)
<pre style="white-space: pre-wrap"><?
print("New Task ID: ", task("example-task", []() {
print("New Task ID: ", task(task_name, []() {
sleep(10);
@@ -68,7 +68,7 @@ RENDER(Request& context)
<pre style="white-space: pre-wrap"><?
print("New Task ID: ", task_repeat("example-task", 5, []() {
print("New Task ID: ", task_repeat(task_name, 5, []() {
sleep(1);
@@ -13,9 +13,9 @@ DTree chat_event(Request& context, String type, String body)
DTree event;
event["type"] = type;
event["body"] = body;
event["connection_id"] = ws_connection_id();
event["scope"] = first(context.params["DOCUMENT_URI"], ws_scope());
event["online"] = (f64)ws_connection_count();
event["connection_id"] = context.params["WS_CONNECTION_ID"];
event["scope"] = first(context.params["WS_DOCUMENT_URI"], context.params["DOCUMENT_URI"]);
event["online"] = float_val(first(context.params["WS_CONNECTION_COUNT"], "0"));
event["at"] = time_format_utc("%H:%M:%S");
event["name"] = context.connection["name"].to_string();
event["message_count"] = context.connection["message_count"];
@@ -37,6 +37,7 @@ RENDER(Request& context)
<pre>Page scope: <?= ws_url ?>
Connected right now: <span id="online-count"><?= online ?></span>
Status: <span id="status">Connecting...</span></pre>
<p>This demo uses <code>context.in</code> for the current payload, <code>context.params["WS_..."]</code> for message metadata, and <code>context.connection</code> for per-client state.</p>
<form id="chat-form" action="?" method="post">
<div>
@@ -152,13 +153,13 @@ WS(Request& context)
if(ws_is_binary())
{
ws_send_to(
ws_connection_id(),
context.params["WS_CONNECTION_ID"],
json_encode(chat_event(context, "notice", "Binary messages are not handled by this demo"))
);
return;
}
DTree payload = json_decode(ws_message());
DTree payload = json_decode(context.in);
String type = clean_chat_field(payload["type"].to_string(), 24);
String name = clean_chat_field(payload["name"].to_string(), 32);
String body = clean_chat_field(payload["body"].to_string(), 500);
@@ -190,6 +191,6 @@ WS(Request& context)
if(type != "")
{
context.connection["last_type"] = type;
ws_send_to(ws_connection_id(), json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
ws_send_to(context.params["WS_CONNECTION_ID"], json_encode(chat_event(context, "notice", "Unknown message type: " + type)));
}
}
+61
View File
@@ -0,0 +1,61 @@
#include "demo_guard.h"
RENDER(Request& context)
{
DTree book;
book["name"] = "book";
book["attrs"]["id"] = "b1";
book["attrs"]["type"] = "reference";
DTree title;
title["name"] = "title";
title["text"] = "UCE & XML";
book["children"].push(title);
DTree chapter;
chapter["name"] = "chapter";
chapter["attrs"]["number"] = "1";
chapter["text"] = "Structural conversion without schema validation.";
book["children"].push(chapter);
String encoded = xml_encode(book);
DTree decoded = xml_decode(encoded);
DTree simple;
simple["title"] = "Hello";
simple["count"] = "3";
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>&#x41;&#66;</symbol></note>";
DTree incoming_tree = xml_decode(incoming);
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
XML
</h1>
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DTree</code> values without schema validation.</p>
<h2>Encoded Element Tree</h2>
<pre><?= encoded ?></pre>
<h2>Decoded DTree</h2>
<pre><?= json_encode(decoded) ?></pre>
<h2>Simple Map Encoding</h2>
<pre><?= xml_encode(simple, "payload") ?></pre>
<h2>Decode Existing XML</h2>
<p>Input XML:</p>
<pre><?= incoming ?></pre>
<p>Decoded DTree:</p>
<pre><?= json_encode(incoming_tree) ?></pre>
<p>
<a href="../doc/index.uce?p=xml_encode">xml_encode() docs</a>
|
<a href="../doc/index.uce?p=xml_decode">xml_decode() docs</a>
</p>
</>
}
+68
View File
@@ -0,0 +1,68 @@
#include "demo_guard.h"
RENDER(Request& context)
{
String source = "# UCE app config\n"
"app:\n"
" name: UCE Starter\n"
" debug: true\n"
" port: 8080\n"
" paths:\n"
" - site\n"
" - cache\n"
"message: |\n"
" Keep config files readable.\n"
" Load them as DTree values.\n";
DTree cfg = yaml_decode(source);
String encoded = yaml_encode(cfg);
DTree roundtrip = yaml_decode(encoded);
DTree generated;
generated["database"]["host"] = "localhost";
generated["database"]["port"] = (f64)3306;
generated["features"]["components"].set_bool(true);
generated["features"]["markdown"].set_bool(true);
DTree theme;
theme = "clean";
generated["themes"].push(theme);
theme = "compact";
generated["themes"].push(theme);
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
YAML
</h1>
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DTree</code> values.</p>
<h2>Config Source</h2>
<pre><?= source ?></pre>
<h2>Decoded DTree</h2>
<pre><?= json_encode(cfg) ?></pre>
<h2>Encoded Again</h2>
<pre><?= encoded ?></pre>
<h2>Generated Config</h2>
<pre><?= yaml_encode(generated) ?></pre>
<h2>Round Trip Reads</h2>
<pre><?
print("app.name = ", roundtrip["app"]["name"].to_string(), "\n");
print("app.debug = ", roundtrip["app"]["debug"].to_bool() ? "true" : "false", "\n");
print("app.port = ", std::to_string(roundtrip["app"]["port"].to_s64()), "\n");
print("app.paths[1] = ", roundtrip["app"]["paths"]["1"].to_string(), "\n");
?></pre>
<p>
<a href="../doc/index.uce?p=yaml_encode">yaml_encode() docs</a>
|
<a href="../doc/index.uce?p=yaml_decode">yaml_decode() docs</a>
</p>
</>
}
+49
View File
@@ -0,0 +1,49 @@
#include "demo_guard.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "ZIP Demo", "create and extract temporary server-side archive files");
return;
}
String base = "/tmp/uce-demo-zip";
String archive = path_join(base, "demo.zip");
String extract_dir = path_join(base, "extract");
mkdir(base);
mkdir(extract_dir);
DTree entries;
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
zip_create(archive, entries);
DTree listing = zip_list(archive);
String hello = zip_read(archive, "hello.txt");
zip_extract(archive, extract_dir);
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
String gz_source = "This string was compressed with gz_compress() and restored with gz_uncompress().";
String gz_body = gz_compress(gz_source);
String gz_roundtrip = gz_uncompress(gz_body);
?><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
</head>
<body>
<h1><a href="index.uce">UCE Test Suite</a> / ZIP</h1>
<p>This demo creates a temporary archive at <code><?= archive ?></code>, lists it, reads one member, and extracts it under <code><?= extract_dir ?></code>.</p>
<h2>zip_list()</h2>
<pre><?= json_encode(listing) ?></pre>
<h2>zip_read()</h2>
<pre><?= hello ?></pre>
<h2>Extracted File</h2>
<pre><?= extracted ?></pre>
<h2>gzip Helpers</h2>
<p>Source bytes: <?= std::to_string((u64)gz_source.size()) ?>; compressed bytes: <?= std::to_string((u64)gz_body.size()) ?></p>
<pre><?= gz_roundtrip ?></pre>
</body>
</html><?
}
+4
View File
@@ -1,3 +1,7 @@
Markup Functions
markdown_to_ast
markdown_to_html
xml_decode
xml_encode
yaml_decode
yaml_encode
+4
View File
@@ -1,6 +1,10 @@
Output / Invocation Functions
coming_from_react
1_RENDER
1_CLI
cli_input
cli_arg
unit_call
component
component_exists
+7
View File
@@ -0,0 +1,7 @@
Regular Expressions
regex_match
regex_search
regex_search_all
regex_replace
regex_split
+8
View File
@@ -0,0 +1,8 @@
SQLite
sqlite_connect
sqlite_disconnect
sqlite_error
sqlite_query
sqlite_insert_id
sqlite_affected_rows
+11
View File
@@ -7,6 +7,12 @@ filter
first
join
json_encode
list_every
list_find
map
list_some
list_sort
list_unique
nibble
print
strpos
@@ -17,6 +23,11 @@ split
split_space
split_utf8
replace
regex_match
regex_search
regex_search_all
regex_replace
regex_split
to_lower
to_upper
trim
+8
View File
@@ -16,3 +16,11 @@ cwd_set
shell_escape
shell_exec
file_unlink
zip_create
zip_list
zip_read
zip_extract
gz_compress
gz_uncompress
server_start_http
server_stop
+1 -1
View File
@@ -1,6 +1,6 @@
Task API
kill
task
task_repeat
task_pid
task_kill
+13 -1
View File
@@ -1,8 +1,20 @@
Types
0_Request
array_merge
DTree
0_DTree
dtree_filter
dtree_group_by
dtree_keys
dtree_map
dtree_omit
dtree_pick
dtree_values
get_by_path
set_status
String
StringList
StringMap
to_bool
to_f64
to_u64
+8
View File
@@ -2,6 +2,14 @@ URI Functions
encode_query
redirect
request_context_params
request_base_url
request_query_path
request_query_route
request_script_url
route_path_is_safe
route_path_normalize
route_path_sanitize
session_id_create
parse_query
uri_decode
+38 -130
View File
@@ -1,104 +1,25 @@
struct DocPage {
String title;
String content;
StringList sig_lines;
StringList param_lines;
StringList see_lines;
};
#include "lib/doc_page.h"
String doc_default_title(String page)
void render_doc_page_link(String page, String label = "", String badge = "")
{
String page_title = page;
if(page_title.length() > 1 && page_title[1] == '_')
nibble(page_title, "_");
return(page_title);
}
page = trim(page);
if(page == "")
return;
if(label == "")
label = doc_index_label(page);
if(badge == "")
badge = doc_page_kind_badge(doc_page_kind(page));
String doc_markdown_inline(String text)
{
text = trim(text);
if(text == "")
return("");
String html = markdown_to_html(text);
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
return(html.substr(3, html.length() - 7));
return(html);
}
String doc_legacy_heading(String section)
{
if(section == "desc")
return("");
if(section == "related")
return("## PHP & JS Equivalents");
return("## " + section);
}
DocPage load_doc_page(String page)
{
DocPage result;
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
String current_section = "";
bool content_mode = false;
StringList content_lines;
for(auto line : lines)
?><a href="index.uce?p=<?= uri_encode(page) ?>"><?= label ?><?
if(badge != "")
{
if(!content_mode && line != "" && line.substr(0, 1) == ":")
{
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
{
current_section = section;
continue;
}
if(section == "content")
{
content_mode = true;
current_section = "content";
continue;
}
current_section = "legacy";
String heading = doc_legacy_heading(section);
if(heading != "")
{
if(content_lines.size() > 0 && content_lines.back() != "")
content_lines.push_back("");
content_lines.push_back(heading);
content_lines.push_back("");
}
continue;
}
if(current_section == "title")
{
if(result.title != "")
result.title += "\n";
result.title += line;
}
else if(current_section == "sig")
{
result.sig_lines.push_back(line);
}
else if(current_section == "params")
{
result.param_lines.push_back(line);
}
else if(current_section == "see")
{
if(trim(line) != "")
result.see_lines.push_back(trim(line));
}
else
{
content_lines.push_back(line);
}
?><span class="badge"><?= badge ?></span><?
}
result.content = join(content_lines, "\n");
result.title = trim(result.title);
return(result);
else
{
?><span class="dim">()</span><?
}
?></a><?
}
void render_doc_params(StringList param_lines)
@@ -130,13 +51,19 @@ void render_see_section(String name)
s32 idx = 0;
for(auto line : lines)
{
line = trim(line);
if(line == "")
{
idx += 1;
continue;
}
if(idx == 0)
{
<><div class="category"><h3><?= line ?></h3><ul></>
}
else if(line != "")
else
{
<><li><a href="index.uce?p=<?= uri_encode(line) ?>"><?= line ?><span class="dim">()</span></a></li></>
?><li><? render_doc_page_link(line); ?></li><?
}
idx += 1;
}
@@ -154,11 +81,19 @@ void render_doc_see_links(StringList see_lines)
{
if(sl[0] == '>')
{
render_see_section(sl.substr(1));
String target = trim(sl.substr(1));
if(doc_has_area(target))
{
render_see_section(target);
}
else if(doc_has_page(target))
{
?><div><? render_doc_page_link(target); ?></div><?
}
}
else
{
?><div><a href="index.uce?p=<?= trim(sl) ?>"><?= trim(sl) ?><span class="dim">()</span></a></div><?
?><div><? render_doc_page_link(trim(sl)); ?></div><?
}
}
?>
@@ -216,36 +151,9 @@ RENDER(Request& context)
for(auto file_name : ls("pages/"))
{
String ft = nibble(file_name, ".");
if(ft.substr(0, 2) == "0_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">struct</span></a></div>
<?
}
else if(ft.substr(0, 2) == "1_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">directive</span></a></div>
<?
}
else if(ft.substr(0, 2) == "3_")
{
String fn = ft;
String pre = nibble(fn, "_");
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= fn ?><span class="badge">info</span></a></div>
<?
}
else
{
?>
<div class="func-item"><a href="?p=<?= uri_encode(ft) ?>"><?= ft ?><span class="dim">()</span></a></div>
<?
}
String label = doc_index_label(ft);
String badge = doc_page_kind_badge(doc_page_kind(ft));
?><div class="func-item"><? render_doc_page_link(ft, label, badge); ?></div><?
}
?></div>
</main>
+153
View File
@@ -0,0 +1,153 @@
#pragma once
struct DocPage {
String title;
String content;
StringList sig_lines;
StringList param_lines;
StringList see_lines;
};
enum class DocPageKind
{
function,
struct_page,
directive,
info
};
String doc_default_title(String page)
{
String page_title = page;
if(page_title.length() > 1 && page_title[1] == '_')
nibble(page_title, "_");
return(page_title);
}
String doc_markdown_inline(String text)
{
text = trim(text);
if(text == "")
return("");
String html = markdown_to_html(text);
if(html.length() >= 7 && html.substr(0, 3) == "<p>" && html.substr(html.length() - 4) == "</p>")
return(html.substr(3, html.length() - 7));
return(html);
}
String doc_legacy_heading(String section)
{
if(section == "desc")
return("");
if(section == "related")
return("## PHP & JS Equivalents");
return("## " + section);
}
bool doc_has_area(String name)
{
return(file_exists("areas/" + name + ".txt"));
}
bool doc_has_page(String name)
{
return(file_exists("pages/" + name + ".txt"));
}
DocPageKind doc_page_kind(String page)
{
if(page.substr(0, 2) == "0_")
return(DocPageKind::struct_page);
if(page.substr(0, 2) == "1_")
return(DocPageKind::directive);
if(page.substr(0, 2) == "3_")
return(DocPageKind::info);
return(DocPageKind::function);
}
String doc_page_kind_badge(DocPageKind kind)
{
if(kind == DocPageKind::struct_page)
return("struct");
if(kind == DocPageKind::directive)
return("directive");
if(kind == DocPageKind::info)
return("info");
return("");
}
String doc_index_label(String page)
{
String label = page;
auto kind = doc_page_kind(page);
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
nibble(label, "_");
return(label);
}
DocPage load_doc_page(String page)
{
DocPage result;
StringList lines = split(file_get_contents("pages/" + page + ".txt"), "\n");
String current_section = "";
bool content_mode = false;
StringList content_lines;
for(auto line : lines)
{
if(!content_mode && line != "" && line.substr(0, 1) == ":")
{
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
{
current_section = section;
continue;
}
if(section == "content")
{
content_mode = true;
current_section = "content";
continue;
}
current_section = "legacy";
String heading = doc_legacy_heading(section);
if(heading != "")
{
if(content_lines.size() > 0 && content_lines.back() != "")
content_lines.push_back("");
content_lines.push_back(heading);
content_lines.push_back("");
}
continue;
}
if(current_section == "title")
{
if(result.title != "")
result.title += "\n";
result.title += line;
}
else if(current_section == "sig")
{
result.sig_lines.push_back(line);
}
else if(current_section == "params")
{
result.param_lines.push_back(line);
}
else if(current_section == "see")
{
if(trim(line) != "")
result.see_lines.push_back(trim(line));
}
else
{
content_lines.push_back(line);
}
}
result.content = join(content_lines, "\n");
result.title = trim(result.title);
return(result);
}
+3 -4
View File
@@ -28,9 +28,8 @@ Map-shaped `DTree` values can also represent list-like data when their keys are
You will encounter `DTree` throughout the runtime, especially in:
- `context.var`
- `context.cfg`
- `context.call`
- `context.props`
- `context.connection`
- `json_decode()` results
- `unit_call()` return values
@@ -107,7 +106,7 @@ For non-map values, `each()` still invokes the callback once:
- `key` is an empty string
```cpp
context.var["items"].each([&](DTree item, String key) {
context.connection["items"].each([&](DTree item, String key) {
print(key, ": ", item.to_string(), "\n");
});
```
@@ -119,7 +118,7 @@ String theme = context.cfg.get_by_path("theme/key").to_string();
u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64();
bool dark_mode = context.call["dark_mode"].to_bool();
bool dark_mode = context.props["dark_mode"].to_bool();
if(DTree* user = payload.key("user")) {
print(user->to_json());
+416 -35
View File
@@ -2,55 +2,436 @@
Request
:sig
Request& context;
Request& context
:see
>types
request_context_params
request_script_url
request_base_url
request_query_path
request_query_route
set_status
component
component_render
unit_render
unit_call
session_start
set_cookie
parse_query
parse_multipart
ws_message
ws_connection_id
ws_connections
ws_send
0_DTree
StringMap
UploadedFile
:content
`Request& context` is the request-local state object passed into UCE handlers. It carries incoming request data, response state, runtime metadata, and helper trees such as `context.cfg`, `context.var`, and `context.call`.
`Request& context` is the request-local state object passed into every UCE handler:
## Core Fields
```cpp
RENDER(Request& context) { ... }
COMPONENT(Request& context) { ... }
WS(Request& context) { ... }
CLI(Request& context) { ... }
SERVE_HTTP(Request* req) { ... }
```
- `ServerState* server`: current server state
- `StringMap params`: FastCGI server parameters
- `StringMap get`: current request GET variables
- `StringMap post`: current request POST variables
- `StringMap cookies`: cookies sent by the browser
- `StringMap session`: current session data
- `String session_id`: session cookie ID
- `String session_name`: session cookie name
- `DTree var`: user-defined request-local data
- `DTree cfg`: request-local configuration tree
- `DTree call`: invocation or message-local structured data
- `DTree connection`: broker-owned WebSocket connection state that persists across `WS(Request& context)` calls for the same socket
- `std::vector<UploadedFile> uploaded_files`: files uploaded in the current request
- `StringMap header`: headers to send back to the browser
- `StringList set_cookies`: cookies queued for the response
- `u64 random_seed`: current request noise seed
- `u64 random_index`: current request noise index position
It is the main bridge between the runtime and page code. It contains incoming request data, response state, output buffers, per-request scratch trees, session/cookie state, WebSocket metadata, and runtime diagnostics.
## Response Control
## Handler Lifetime
`context.set_status(s32 code[, String reason])` sets the HTTP status line and updates `context.flags.status`. When `reason` is omitted, UCE uses a built-in standard reason phrase for common status codes.
A fresh `Request` is created for each HTTP/CLI/custom-server request. Component and unit calls normally share that same object, so state placed on `context.call`, `context.header`, `context.session`, or `context.cfg` is visible to later components in the same request.
## Flags And Stats
For WebSockets, each incoming message is delivered as its own `Request`, but `context.connection` points at broker-owned per-socket state that persists for the lifetime of that WebSocket connection.
- `bool flags.log_request`: controls whether the request should be logged
- `u32 stats.bytes_written`
- `f64 stats.time_init`
- `f64 stats.time_start`
- `f64 stats.time_end`
`ONCE(Request& context)` hooks run once per request, per resolved unit file, before the first `RENDER`, `COMPONENT`, `CLI`, or matching entrypoint from that unit.
## Common Usage Notes
## Incoming Request Maps
- `context.cfg` is the usual place for structured configuration. Use `context.cfg.get_by_path("path/to/value")` for deep reads.
- `context.call` carries invocation data for component calls, unit calls, and WebSocket messages.
- `context.connection` is only meaningful for WebSocket traffic and persists for the lifetime of the connection.
### `context.params` — server/runtime parameters
`unit_render(String file_name, [Request& context])` invokes another UCE file using the current or supplied request context.
Type: `StringMap`
This is the low-level parameter map from FastCGI/direct HTTP plus UCE-populated convenience fields. It is closest to PHP `$_SERVER`.
Common CGI/FastCGI-style keys include:
- `REQUEST_METHOD`: `GET`, `POST`, etc.
- `REQUEST_URI`: raw request URI where available
- `DOCUMENT_URI`: normalized request path where available
- `SCRIPT_NAME`: script path where available
- `SCRIPT_FILENAME`: resolved filesystem path of the active UCE unit
- `QUERY_STRING`: raw query string
- `DOCUMENT_ROOT`: web root used by the frontend/backend
- `CONTENT_TYPE`: request body content type
- `CONTENT_LENGTH`: request body length
- `HTTP_COOKIE`: raw cookie header
- `HTTP_HOST`, `HTTP_USER_AGENT`, `HTTP_ACCEPT`, and other `HTTP_...` headers supplied by the frontend
UCE also populates convenience route/link fields before handlers run:
- `SCRIPT_URL`: canonical script URL; `/index.uce` is collapsed to the containing directory URL
- `BASE_URL`: canonical directory URL for the script
- `ROUTE_PATH`: sanitized first keyless query-string segment, defaulting to `index` when no route was supplied; empty when unsafe input was rejected
- `ROUTE_PAGE`: first segment of `ROUTE_PATH`
- `ROUTE_PATH_RAW`: normalized but untrusted route input, for diagnostics only
- `ROUTE_VALID`: `1` when route input is safe, `0` when the supplied route was rejected
See `request_context_params`, `request_script_url`, `request_base_url`, `request_query_path`, `request_query_route`, and `route_path_sanitize`.
### `context.get`
Type: `StringMap`
Parsed query-string key/value parameters. This is populated from `context.params["QUERY_STRING"]` with `parse_query()`.
```cpp
String theme = first(context.get["theme"], "default");
```
For front-controller route URLs such as `/?dashboard&theme=dark`, the keyless `dashboard` segment is represented by sanitized `ROUTE_PATH`; named parameters such as `theme=dark` are available in `context.get`.
### `context.post`
Type: `StringMap`
Parsed request body parameters for ordinary `POST` requests. URL-encoded bodies are parsed with `parse_query()`. Multipart form data is parsed with `parse_multipart()` and uploaded files are listed in `context.uploaded_files`.
```cpp
if(context.params["REQUEST_METHOD"] == "POST")
String email = context.post["email"];
```
### `context.cookies`
Type: `StringMap`
Cookies sent by the client, parsed from `HTTP_COOKIE`. `set_cookie()` also updates this map after queuing a response cookie, so later code in the same request can observe the new value.
### `context.in`
Type: `String`
Raw request body. For WebSocket handlers, this is the current message payload.
Use this for JSON APIs:
```cpp
DTree body = json_decode(context.in);
```
### `context.uploaded_files`
Type: `std::vector<UploadedFile>`
Each `UploadedFile` contains:
- `file_name`: original submitted filename
- `tmp_name`: temporary server-side upload path
- `size`: uploaded byte count
Use this with multipart form posts.
## Session State
### `context.session`
Type: `StringMap`
Session data loaded by `session_start()`. UCE does not load sessions automatically for every request; call `session_start()` before reading/writing session data.
```cpp
session_start();
context.session["user_id"] = "42";
```
At the end of a successful request, modified session data is saved automatically if a session is active.
### `context.session_id` and `context.session_name`
The active session ID and cookie name after `session_start()`.
Related helpers:
- `session_start()`
- `session_destroy()`
- `session_id_create()`
- `set_cookie()`
## Per-request Structured Trees
### `context.call`
Type: `DTree`
General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components.
Examples from front-controller style apps:
```cpp
context.call["route"] = request_query_route(context);
context.call["app"]["page_type"] = "html";
context.call["fragments"]["main"] = captured_html;
```
Prefer clear top-level names when state is app-wide (`route`, `fragments`) and nested app names only when the state is truly owned by that app (`app/page_title`, `app/page_type`).
### `context.cfg`
Type: `DTree`
Request-local structured configuration. The runtime does not fill this with application config by default; application code may assign it during boot/setup:
```cpp
context.cfg = get_config();
```
This is separate from `context.server->config`, which is the runtime/server string config from `/etc/uce/settings.cfg`.
Use `get_by_path()` for non-mutating deep reads:
```cpp
String site_name = context.cfg.get_by_path("site/name").to_string();
```
### `context.props`
Type: `DTree`
Invocation-local props for `component()`, `component_render()`, and macro-style `unit_call()` entrypoints. During a component call, the runtime temporarily replaces `context.props` with the props passed to that component and restores the previous value after the call returns.
```cpp
DTree props;
props["title"] = "Dashboard";
print(component("components/card", props, context));
```
### `context.connection`
Type: `DTree`
WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket.
```cpp
context.connection["message_count"] = context.connection["message_count"].to_u64() + 1;
```
Only meaningful for WebSocket handlers.
## Response State
### `context.response_code`
Type: `String`
The raw status line. Usually use `context.set_status(...)` instead of writing this directly.
### `context.header`
Type: `StringMap`
Response headers to emit. Header names are case-sensitive as written.
```cpp
context.header["Content-Type"] = "application/json";
context.header["Location"] = "/info/";
```
### `context.set_cookies`
Type: `StringList`
Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this directly.
### `context.set_status(code[, reason])`
Sets the HTTP response status and `context.flags.status`.
```cpp
context.set_status(404, "Not Found");
context.set_status(302, "Found");
context.header["Location"] = app_link("dashboard", context);
```
Related helpers:
- `redirect(url[, code])`
- `set_cookie(...)`
## Output Buffers
### `context.ob_stack` and `context.ob`
Internal output-buffer stack. Most code should use helpers instead of touching these directly:
- `print(...)`
- `out(...)`
- `ob_start()`
- `ob_get()`
- `ob_get_close()`
- `ob_close()`
Common capture pattern:
```cpp
ob_start();
print(component("views/dashboard", context));
String html = ob_get_close();
context.call["fragments"]["main"] = html;
```
### `context.out` and `context.err`
Runtime output/error artifacts used by some transports and failure paths. Normal page rendering should use `print()` / output buffers.
## Request Flags
`context.flags` contains runtime booleans and the numeric status:
- `log_request`: whether the request should be logged
- `is_finished`: internal completion marker
- `status`: numeric HTTP status set by `set_status()`
- `output_closed`: internal transport state
- `params_closed`: internal transport state
- `input_closed`: internal transport state
Most page code only reads `flags.status`, if anything.
## Request Stats
`context.stats` contains counters/timing for the request:
- `bytes_written`
- `time_init`
- `time_start`
- `time_end`
- `mem_high`
- `mem_alloc`
- `invoke_count`
These are useful for diagnostics, demos, and runtime instrumentation.
## Random / Noise State
- `random_seed`
- `random_index`
Used by UCE noise/random helpers to provide request-local deterministic progression.
Related helpers include functions in the noise/hash area such as `gen_int`, `gen_float`, `gen_noise64`, and `gen_sha1`.
## WebSocket Fields
In `WS(Request& context)`, the runtime mirrors WebSocket metadata into `context.params` and `context.resources`.
Convenience `context.params` keys include:
- `WS_MESSAGE`
- `WS_CONNECTION_ID`
- `WS_SCOPE`
- `WS_CONNECTION_COUNT`
- `WS_OPCODE`
- `WS_MESSAGE_TYPE`
- `WS_DOCUMENT_URI`
Prefer WebSocket helper functions where possible:
- `ws_message()`
- `ws_connection_id()`
- `ws_scope()`
- `ws_opcode()`
- `ws_is_binary()`
- `ws_connections()`
- `ws_connection_count()`
- `ws_send()`
- `ws_send_to()`
- `ws_close()`
Use `context.connection` for per-socket structured state.
## Runtime / Resource Fields
### `context.server`
Pointer to server state. Useful mainly for low-level/runtime code. Runtime config lives at:
```cpp
context.server->config["KEY"]
```
This is a `StringMap`, separate from app-owned `context.cfg`.
### `context.resources`
Internal runtime resources and transport state. Includes sockets, MySQL handles, WebSocket state, current unit file, and parser buffers. Application code should normally use public helpers instead of editing this directly.
Notable fields:
- `is_websocket`
- `is_cli`
- `websocket_connection_id`
- `websocket_scope`
- `websocket_scope_connection_ids`
- `current_unit_file`
## Common Patterns
### Minimal page
```cpp
RENDER(Request& context)
{
<><h1>Hello <?= context.get["name"] ?></h1></>
}
```
### JSON endpoint
```cpp
RENDER(Request& context)
{
context.header["Content-Type"] = "application/json";
DTree response;
response["ok"].set_bool(true);
print(json_encode(response));
}
```
### Redirect
```cpp
RENDER(Request& context)
{
context.set_status(302, "Found");
context.header["Location"] = "/info/";
}
```
### Component props
```cpp
DTree props;
props["title"] = "Welcome";
print(component("components/card", props, context));
```
### Front-controller route
```cpp
context.call["route"] = request_query_route(context);
String route_path = context.call["route"]["l_path"].to_string();
```
Or use runtime-populated params directly:
```cpp
String route_path = context.params["ROUTE_PATH"];
```
## Related Concepts
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, and `http_response_code()`
- JavaScript / Node.js: Express `req` and `res`, Fetch `Request`, `Headers`, cookies or session middleware, and per-connection state in WebSocket handlers
- PHP: `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`, `$_SESSION`, `header()`, output buffering, and `http_response_code()`
- JavaScript / Node.js: Express `req`/`res`, Fetch `Request`/`Response`, route params, middleware-populated locals, and per-socket WebSocket state
+67
View File
@@ -0,0 +1,67 @@
:title
CLI
:sig
CLI(Request& context)
:see
>1_RENDER
>1_COMPONENT
>1_WS
>1_INIT
>1_ONCE
>unit_call
>cli_input
>cli_arg
:content
Defines a local command-line entrypoint for a UCE unit.
`CLI(Request& context)` is invoked only through the local UCE CLI Unix socket, not through ordinary public HTTP requests. This lets web apps keep test runners, migrations, maintenance tasks, and admin tooling beside the rest of their UCE units while still separating those commands from browser-facing `RENDER()` routes.
The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_SOCKET_PATH`.
Example convenience script usage:
```sh
scripts/uce-cli /tests/cli.uce
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
```
Equivalent curl probe:
```sh
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
```
For structured commands, prefer JSON POST bodies. The `scripts/uce-cli` helper sends `key=value` parameters as JSON POST by default, while still allowing `--get` for simple query-string probes.
Inside the handler, the usual `Request& context` fields are available:
- `context.get` for command query parameters
- `context.post` and `context.in` for POST bodies
- `context.params["UCE_CLI"] == "1"` for CLI socket dispatch
- `context.params["SCRIPT_FILENAME"]` for the invoked unit file
CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly.
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`.
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
String action = first(input["action"].to_string(), "ping");
if(action == "ping")
{
print("ok\n");
return;
}
context.set_status(400, "Bad Command");
print("unknown action: ", action, "\n");
}
```
`ONCE(Request& context)` runs before `CLI()` in the same way it runs before render and component entrypoints. `INIT(Request& context)` runs when the unit is loaded into a worker.
+9 -3
View File
@@ -8,6 +8,8 @@ COMPONENT(Request& context)
>component
>component_render
>1_RENDER
>1_INIT
>1_ONCE
>1_WS
:content
@@ -15,6 +17,10 @@ Defines the default component entrypoint for the current `.uce` file.
`component()` and `component_render()` call `COMPONENT(Request& context)` by default. Named component entrypoints use `COMPONENT:NAME(Request& context)`.
If the same file defines `ONCE(Request& context)`, that hook runs once per request before the first `COMPONENT()` or `COMPONENT:NAME()` call for that unit.
If the file defines `INIT(Request& context)`, that hook runs once when the worker loads the compiled unit into memory.
## Why It Exists
This keeps page rendering and component rendering separate:
@@ -25,7 +31,7 @@ This keeps page rendering and component rendering separate:
A file intended only for component reuse can define `COMPONENT()` without defining `RENDER()`.
Inside component handlers, props arrive through `context.call`.
Inside component handlers, props arrive through `context.props`.
When you call `component(":NAME", props, context)` or `component_render(":NAME", props, context)`, the runtime resolves `:NAME` against the current `.uce` file instead of requiring the file name again.
@@ -34,12 +40,12 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
```cpp
COMPONENT(Request& context)
{
<><section><?: component(":BODY", context.call, context) ?></section></>
<><section><?: component(":BODY", context.props, context) ?></section></>
}
COMPONENT:BODY(Request& context)
{
<><p><?= context.call["body"] ?></p></>
<><p><?= context.props["body"] ?></p></>
}
```
+50
View File
@@ -0,0 +1,50 @@
:title
INIT
:sig
INIT(Request& context)
:see
>1_COMPONENT
>1_ONCE
>1_RENDER
>1_WS
>3_C++ Preprocessor
>unit_call
:content
Defines a worker-load hook for the current `.uce` unit.
When a worker loads the unit's compiled shared object into memory, the runtime checks whether the unit exposes `INIT(Request& context)`. If it does, the hook runs once for that load before the unit begins serving later requests from that in-memory copy.
Because UCE usually loads units on demand during a request, `INIT()` still receives a valid `Request& context`. Use it for worker-local initialization, not for request-local state that should reset each request.
## Typical Uses
- warm caches or parse static lookup data into globals
- initialize worker-local helper state for expensive component trees
- perform one-time registration work for that unit's in-memory copy
## Example
```cpp
std::map<String, String> cached_labels;
INIT(Request& context)
{
if(cached_labels.empty())
cached_labels["ready"] = "Ready";
}
COMPONENT(Request& context)
{
<>
<p><?= cached_labels["ready"] ?></p>
</>
}
```
## Related Concepts
- PHP: opcode-cache preload or one-time bootstrap work per worker process
- JavaScript / Node.js: module-load initialization or lazy singleton setup
+49
View File
@@ -0,0 +1,49 @@
:title
ONCE
:sig
ONCE(Request& context)
:see
>1_COMPONENT
>1_INIT
>1_RENDER
>1_WS
>3_C++ Preprocessor
>unit_call
:content
Defines a request-local one-time hook for the current `.uce` unit.
When a request first enters a given file through `RENDER(Request& context)`, `COMPONENT(Request& context)`, or any `COMPONENT:NAME(Request& context)` handler, the runtime checks whether that unit exposes `ONCE(Request& context)`. If it does, the hook runs before the selected render or component handler.
`ONCE()` is tracked per request and per resolved unit file, so repeated component calls to the same file inside one request do not rerun it.
## Typical Uses
- prepare request-local derived state on `context.call`
- load request-scoped config or data needed by multiple named component handlers
- normalize shared props before the unit's first render/component call
## Example
```cpp
ONCE(Request& context)
{
context.call["card_defaults"]["tone"] = "info";
}
COMPONENT(Request& context)
{
<>
<div class="card card-<?= context.call["card_defaults"]["tone"] ?>">
<?: component(":BODY", context.props, context) ?>
</div>
</>
}
```
## Related Concepts
- PHP: per-request bootstrap work before a template or partial first runs
- JavaScript / Node.js: request-scoped lazy initialization before a route or component render
+11 -3
View File
@@ -6,6 +6,10 @@ RENDER(Request& context)
:see
>ob
>1_COMPONENT
>1_INIT
>1_ONCE
>1_WS
:content
Defines the main HTTP render handler for the current `.uce` page.
@@ -16,11 +20,15 @@ When a page is requested over HTTP, the runtime loads the target file and calls
The default page entrypoint is always the plain `RENDER(Request& context)` handler.
Reusable component handlers now live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
Reusable component handlers live on `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)`. The component helpers call those handlers, not `RENDER()`.
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.call` tree.
The request environment is passed explicitly through `context`, including params, cookies, post data, session state, headers, uploaded files, and the current `context.props` tree.
For a normal direct page request, `context.call` starts empty.
If the file defines `ONCE(Request& context)`, the runtime calls that hook once per request before the first `RENDER()` or `COMPONENT...` entrypoint from that unit runs.
If the file defines `INIT(Request& context)`, the runtime calls that hook once when the worker loads the compiled unit into memory.
For a normal direct page request, `context.props` starts empty.
If the page is invoked from another UCE file via `unit_render(file_name, context)`, the callee receives that same `context`.
+19 -7
View File
@@ -6,13 +6,17 @@ WS(Request& context)
:see
>websocket
>1_COMPONENT
>1_INIT
>1_ONCE
>1_RENDER
:content
Defines the WebSocket message handler for the current `.ws.uce` page.
The same page may expose both `RENDER(Request& context)` and `WS(Request& context)`. `RENDER(Request& context)` serves the initial HTTP response, while `WS(Request& context)` is called whenever a complete WebSocket message arrives for that page.
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. Use `context.call`, `context.connection`, `ws_opcode()`, and `ws_is_binary()` to inspect the current message.
UCE reassembles fragmented messages before calling `WS(Request& context)`. Text and binary frames are both delivered. The current payload is available directly on `context.in`, message metadata is mirrored into `context.params["WS_..."]`, and connection-local state lives on `context.connection`.
## Connection State
@@ -20,13 +24,21 @@ UCE reassembles fragmented messages before calling `WS(Request& context)`. Text
## Message Data
The current message data is available in `context.call`:
The current message payload is available as:
- `context.call["message"]`: current message payload
- `context.call["connection_id"]`: sender connection ID
- `context.call["scope"]`: current endpoint scope
- `context.call["opcode"]`: WebSocket opcode of the current message
- `context.call["document_uri"]`: request URI of the current endpoint
- `context.in`: current WebSocket payload
- `context.params["WS_MESSAGE"]`: same payload mirrored into the parameter map
The current message metadata is available as:
- `context.params["WS_CONNECTION_ID"]`: sender connection ID
- `context.params["WS_SCOPE"]`: current endpoint scope
- `context.params["WS_CONNECTION_COUNT"]`: number of currently connected clients in that scope
- `context.params["WS_OPCODE"]`: WebSocket opcode of the current message
- `context.params["WS_MESSAGE_TYPE"]`: `TEXT` or `BINARY`
- `context.params["WS_DOCUMENT_URI"]`: request URI of the current endpoint
Helper wrappers such as `ws_message()`, `ws_connection_id()`, `ws_scope()`, `ws_connection_count()`, `ws_opcode()`, and `ws_is_binary()` are still available when that reads better for the handler.
## Related Concepts
+42 -7
View File
@@ -8,7 +8,7 @@ UCE source preprocessing
load
unit_render
unit_call
0_context
0_Request
1_COMPONENT
:content
@@ -25,7 +25,8 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
- `#load "other.uce"` injects another UCE unit at compile time.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `ONCE`, `RENDER`, and `COMPONENT` may be followed by a preprocessor attribute line such as `@fragment head` before the opening `{`. The handler's output is then captured and appended to `context.call["fragments"]["head"]` instead of being emitted at the call site. `ONCE` defaults to `@fragment once` when no fragment is specified.
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
@@ -34,7 +35,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- The generated file starts by including `COMPILER_SYS_PATH/src/lib/uce_lib.h`.
- It then inlines the configured setup template from `SETUP_TEMPLATE` (by default `scripts/setup.h.template`), which defines the internal hook `__uce_set_current_request(Request*)`.
- It inserts `#line 1` before page code so compiler diagnostics point back to the original `.uce` file.
- Each literal region is rewritten into one or more `print(R"( ... )");` calls.
- Each literal region is rewritten into one or more `print(R"...( ... )...");` calls using a safe raw-string delimiter selected for that literal content.
- `<>` and `?>` both switch from code mode into literal output.
- `</>` and `<?` both switch from literal output back into code mode.
- `<? ... ?>` temporarily breaks out of literal printing, emits the enclosed C++ unchanged, then resumes literal output.
@@ -42,10 +43,13 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- `<?: ... ?>` becomes `print(...);` and is intended for trusted markup or already-escaped content.
- `#load "file.uce"` is replaced with a generated C++ `#include` that points at the loaded unit's preprocessed `.cpp` file under `BIN_DIRECTORY`.
- Lines beginning with `EXPORT` are scanned so their declarations can be written to a sibling `.exports.txt` file.
- `@fragment slot-name` lines immediately following `ONCE`, `RENDER`, or `COMPONENT` are removed and replaced with an output-capture guard at the start of the handler body.
- Lines beginning with `RENDER:NAME(...)` are rewritten into exported `__uce_render_NAME(...)` functions.
- Lines beginning with `COMPONENT:NAME(...)` are rewritten into exported `__uce_component_NAME(...)` functions for the component helpers.
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
- When a worker loads the compiled unit into memory, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side load.
- On each request, the first time a given unit is entered through `RENDER()`, `CLI()`, or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the selected handler.
## Generated Files
@@ -88,7 +92,7 @@ Literal output with trusted unescaped markup:
```cpp
RENDER(Request& context)
{
<><div class="panel"><?: component("components/card", context.call, context) ?></div></>
<><div class="panel"><?: component("components/card", context.props, context) ?></div></>
}
```
@@ -96,7 +100,7 @@ Roughly becomes:
```cpp
print(R"(<div class="panel">)");
print(component("components/card", context.call, context));
print(component("components/card", context.props, context));
print(R"(</div>)");
```
@@ -113,15 +117,44 @@ RENDER(Request& context)
The loaded file is resolved relative to the current source file unless the path is already absolute.
One-time worker initialization plus request-local setup:
```cpp
INIT(Request& context)
{
// load worker-local data, warm caches, or initialize globals for this unit
}
ONCE(Request& context)
{
// prepare request-local state before the first render/component call
context.call["page_title"] = "Demo";
}
```
One-time page assets captured for a template-controlled slot:
```cpp
ONCE(Request& context)
@fragment head
{
?><link rel="stylesheet" href="/assets/page.css" /><?
}
```
The page template can then render `context.call["fragments"]["head"]` inside `<head>`.
## Rules
- Literal mode can start on either `<>` or `?>`.
- Literal mode can end on either `</>` or `<?`.
- Literal delimiters are interchangeable; the parser now treats them as one shared code-vs-literal state machine rather than as separate nested block types.
- Literal delimiters are interchangeable; the parser treats them as one shared code-vs-literal state machine rather than as separate nested block types.
- `#load` is recognized only when the current line starts with `#load ` at column 1.
- `EXPORT` harvesting only triggers when the current line starts with `EXPORT` at column 1 and is followed by whitespace.
- Relative `#load` paths are expanded against the including unit's source directory.
- `unit_render()` and `unit_call()` are runtime APIs. `#load` is a compile-time composition feature.
- `INIT()` runs when the shared object is loaded into a worker during a request-triggered load, so it still receives a valid `Request& context`.
- `ONCE()` is tracked per request and per resolved unit file. A file entered multiple times in one request only runs `ONCE()` once.
## Limitations
@@ -129,13 +162,15 @@ The loaded file is resolved relative to the current source file unless the path
- Outside literal blocks it tracks C++ quotes and comments while deciding whether `<>` or `?>` should open literal mode.
- It does not understand comments, raw string literals, templates, or general C++ token structure.
- Inside literal blocks it tracks quotes and comments while scanning `<? ... ?>`, `<?= ... ?>`, and `<?: ... ?>` islands so quoted `?>` text does not close those islands early.
- Because literal output is emitted as a C++ raw string literal `R"( ... )"`, literal content must not contain the exact terminator sequence `)"` or the generated C++ will break.
- Literal output is emitted through C++ string literals generated by the preprocessor. The preprocessor chooses a raw-string delimiter that does not occur in the literal content, so literal text may safely contain the ordinary raw-string terminator sequence `)"`.
- `#load` depends on the target unit's generated `.cpp` existing and being compilable. If the target cannot be preprocessed or compiled correctly, the including file will fail to compile as well.
## Debugging
- Inspect the generated file under `BIN_DIRECTORY` first. That file shows the exact C++ produced by the UCE preprocessor.
- Compiler errors usually point back to the `.uce` source because the preprocessor inserts `#line 1`, but the generated `.cpp` is still the best place to inspect expansion problems.
- Compile failures are reported with the source path, generated C++ path, compile-output artifact path, an excerpt when UCE can identify a line, and the raw compiler output from the configured compile script.
- Runtime request failures include the request/script path, generated C++ path, a hint about inspecting template delimiters and recent component/unit calls, and a native trace when available.
- If a `#load` include looks wrong, check the current file's directory, the configured `BIN_DIRECTORY`, and whether the loaded page already produced its own generated `.cpp`.
## Related Concepts
+4
View File
@@ -3,6 +3,10 @@ String
:see
>types
>string
split_utf8
substr
html_escape
:content
Primary string type used throughout UCE.
+25
View File
@@ -0,0 +1,25 @@
:sig
StringList
:see
>types
String
split
split_space
split_utf8
join
regex_split
:content
Sequential container of `String` values.
`StringList` is an alias for `std::vector<String>`.
It is returned by split-style helpers such as `split()`, `split_space()`, `split_utf8()`, and `regex_split()`.
Use `join()` when you want to turn a `StringList` back into a single `String`.
Related:
- PHP: indexed arrays of strings
- JavaScript / Node.js: arrays of strings
+4
View File
@@ -3,6 +3,10 @@ StringMap
:see
>types
0_Request
parse_query
encode_query
array_merge
:content
Associative container mapping `String` keys to `String` values.
+3
View File
@@ -9,6 +9,9 @@ return value : merged result
:see
>types
0_DTree
StringMap
json_decode
:content
Merges two maps or trees using PHP-like merge behavior.
+24
View File
@@ -0,0 +1,24 @@
:title
cli_arg
:sig
String cli_arg(Request& context, String key, String default_value = "")
:see
>1_CLI
>cli_input
:content
Reads one value from the merged `cli_input(context)` parameter tree.
If the key is missing, or the resolved value is empty, `default_value` is returned.
```cpp
CLI(Request& context)
{
String action = cli_arg(context, "action", "help");
print("action=", action, "\n");
}
```
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly.
+41
View File
@@ -0,0 +1,41 @@
:title
cli_input
:sig
DTree cli_input(Request& context)
:see
>1_CLI
>json_decode
>DTree
:content
Returns a structured parameter tree for a `CLI(Request& context)` invocation.
`cli_input()` merges simple command inputs into one `DTree`:
1. query parameters from `context.get`
2. form parameters from `context.post`
3. JSON object fields from an `application/json` or `*+json` request body
Later sources override earlier ones, so a JSON body can override a query or form key with the same name.
If the JSON body is a scalar or array instead of an object, the decoded value is stored under `input["_"]`.
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
String action = first(input["action"].to_string(), "help");
if(action == "echo")
print(input["message"].to_string(), "\n");
}
```
Convenience script usage:
```sh
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
```
+69
View File
@@ -0,0 +1,69 @@
:title
Coming from React, Next, or Remix
:sig
UCE orientation for React-framework developers
:see
1_RENDER
1_COMPONENT
component
unit_render
3_C++ Preprocessor
map
filter
dtree_filter
:content
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
## Concept Map
- `RENDER(Request& context)` is the page/server-render entrypoint.
- `COMPONENT(Request& context)` and `COMPONENT:NAME(Request& context)` are server-rendered components.
- `context.props` is the component invocation payload, similar to props.
- `context.call` is request-local scratch state shared across units during one request.
- `context.cfg` is structured app/config data.
- `ONCE(Request& context)` is per-request setup for a unit before its first render/component entry.
- `INIT(Request& context)` is worker-local setup when a unit is loaded.
- `<?= expression ?>` is escaped interpolation; prefer it for user-visible text.
- `<?: expression ?>` is trusted raw markup output, closer to a deliberate `dangerouslySetInnerHTML` decision.
- `unit_render()` renders another page unit; `component()` returns component HTML as a string.
## Routes and Layouts
UCE does not require a framework-level router. A front controller can keep routing explicit and app-local. The starter example demonstrates this in `site/examples/uce-starter/index.uce`: it resolves a request path by checking:
1. `views/<path>.uce`
2. `views/<path>/index.uce`
3. parent index handlers such as `views/workspace/index.uce` with the last segment as a route parameter
That keeps file-based and hierarchical routing in normal UCE code instead of hiding it in the runtime.
## Data Shaping Near Render Code
The function library includes small collection helpers for common route/menu/card transformations:
```cpp
auto visible = filter(routes, [](String route) { return(route != "admin"); });
auto labels = map(visible, [](String route) { return(to_upper(route)); });
DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
```
Use these when the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern.
## Assets and Islands
Global runtime APIs for assets and islands are intentionally not part of UCE core. The starter emits CSS and JavaScript from the owning unit's `ONCE(Request& context)` hook, with a few shared sibling asset components when multiple components need the same files. The only starter web-affordance helper left is `COMPONENT:island` in `components/theme/web_affordances.uce` for small progressive-enhancement modules. This keeps app policy in the app without an asset registry layer.
## Debugging
When a unit fails to compile, UCE reports the source path, generated C++ path, compile-output artifact, a source/generated excerpt when it can identify a line, and the raw compiler output. The generated C++ under `BIN_DIRECTORY` is the source of truth for what the configured compiler actually saw.
## What Not To Expect
- No client-side virtual DOM is built into UCE.
- No global file-router is imposed by the runtime.
- No JSX-like component tags are required for this workflow.
- Component children/slot syntax is intentionally deferred; use explicit props and component calls for now.
+69 -2
View File
@@ -3,13 +3,16 @@ String component(String name, [DTree props], [Request& context])
:see
>ob
>component_render
>1_COMPONENT
>1_RENDER
:content
Renders another `.uce` file as a component and returns the captured output as a `String`.
`component()` resolves the target file relative to the current page and also tries the `components/` prefix automatically, mirroring the shorthand used by the starter example project.
Component props are passed in `context.call`.
Component props are passed in `context.props`.
Because `<?= ... ?>` HTML-escapes its value, embed component markup with `<?: component(...) ?>`, `print(component(...))`, or use `component_render(...)` for direct output.
@@ -21,13 +24,17 @@ The default handler is `COMPONENT(Request& context)`.
When `name` starts with a colon, such as `:BODY`, the target resolves against the current `.uce` file so component files can call their own named handlers without repeating the file name.
When a component unit defines `ONCE(Request& context)`, the runtime calls that hook once per request, per resolved component file, before the first `COMPONENT()` or `COMPONENT:NAME()` handler from that file runs.
## Resolution Order
- exact file name
- exact file name with `.uce`
- the same two forms under `components/`
## Example
## Common Patterns
Default component handler:
```cpp
DTree props;
@@ -36,6 +43,66 @@ props["title"] = "Status";
<><?: component("workspace/panel", props, context) ?></>
```
Named component handler:
```cpp
DTree props;
props["title"] = "System";
props["body"] = "Healthy";
print(component("components/card:BODY", props, context));
```
Self-targeted named handler from inside the same file:
```cpp
COMPONENT(Request& context)
{
<>
<section class="card">
<?: component(":BODY", context.props, context) ?>
</section>
</>
}
COMPONENT:BODY(Request& context)
{
<>
<p><?= context.props["body"] ?></p>
</>
}
```
Preparing props in C++ before rendering:
```cpp
DTree props;
props["items"][0] = "alpha";
props["items"][1] = "beta";
props["items"][2] = "gamma";
String html = component("components/list", props, context);
print(html);
```
Embedding returned component markup inside a literal block:
```cpp
<>
<div class="panel">
<?: component("components/card", props, context) ?>
</div>
</>
```
Because `<?= ... ?>` escapes HTML, use `<?: ... ?>` when inserting the returned markup from `component()`.
## Lifecycle Notes
- `INIT(Request& context)` runs once when the worker loads that unit into memory.
- `ONCE(Request& context)` runs once per request before the first component or render entrypoint from that file.
- `component()` then calls either `COMPONENT(Request& context)` or the selected `COMPONENT:NAME(Request& context)` handler.
## Related Concepts
- PHP: reusable template partials or helper-rendered view fragments returned as strings
+4
View File
@@ -3,6 +3,10 @@ bool component_exists(String name)
:see
>ob
component
component_render
component_resolve
1_COMPONENT
:content
Checks whether a component file can be resolved from the current page context.
+7 -1
View File
@@ -3,16 +3,22 @@ void component_render(String name, [DTree props], [Request& context])
:see
>ob
component
component_exists
component_resolve
1_COMPONENT
:content
Renders another `.uce` file as a component and writes the result directly to the current output buffer.
This is the direct-output counterpart to `component()`.
Component props are passed through `context.call`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
Component props are passed through `context.props`, and `name:COMPONENTFUNC` may be used to select a named handler exported by `COMPONENT:COMPONENTFUNC(Request& context)`.
When `name` starts with `:`, the runtime resolves that named handler against the current `.uce` file.
If the target file defines `ONCE(Request& context)`, that hook runs once per request before the file's first component or render entrypoint.
Use `component_render()` when you want to write component output directly from C++ code instead of capturing it as a `String`.
## Example
+4
View File
@@ -3,6 +3,10 @@ String component_resolve(String name)
:see
>ob
component
component_exists
component_render
1_COMPONENT
:content
Resolves a component name to the concrete `.uce` file path that will be loaded.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_filter
:sig
DTree dtree_filter(DTree tree, function<bool (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Keeps children for which f returns true. List-like input stays list-like.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree visible = dtree_filter(items, [](DTree item, String key) { return(item["hidden"].to_bool() == false); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_group_by
:sig
DTree dtree_group_by(DTree tree, function<String (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Groups children into list-like buckets by the string returned from f.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_keys
:sig
StringList dtree_keys(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns map keys from a DTree. Scalar values produce an empty list.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
StringList keys = dtree_keys(context.cfg["menu"]);
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_map
:sig
DTree dtree_map(DTree tree, function<DTree (DTree, String)> f)
:see
StringList
0_DTree
filter
:content
Transforms each child. List-like input stays list-like; map input keeps keys.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree titles = dtree_map(items, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); });
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_omit
:sig
DTree dtree_omit(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies a DTree map except for selected keys.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree safe_user = dtree_omit(user, {"password_hash"});
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_pick
:sig
DTree dtree_pick(DTree tree, StringList keys)
:see
StringList
0_DTree
filter
:content
Copies only selected keys from a DTree map.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree public_user = dtree_pick(user, {"name", "avatar"});
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.
+26
View File
@@ -0,0 +1,26 @@
:title
dtree_values
:sig
DTree dtree_values(DTree tree)
:see
StringList
0_DTree
filter
:content
Returns child values as a list-like DTree.
These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary.
```cpp
DTree menu_items = dtree_values(context.cfg["menu"]);
```
Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern.
## Related Concepts
- JavaScript / React: `Array.map`, `Array.filter`, `Array.find`, object `pick`/`omit`, and grouping route or navigation records before rendering.
- PHP: `array_map`, `array_filter`, `array_unique`, and associative array projection.

Some files were not shown because too many files have changed in this diff Show More