Compare commits

..

72 Commits

Author SHA1 Message Date
udo
b04ce3d005 Profile logical MySQL operations 2026-07-16 16:31:13 +00:00
udo
9dc9c61af6 Keep proactive freshness scans off requests 2026-07-16 16:06:02 +00:00
udo
ec07b5df22 Cache component freshness probes 2026-07-16 15:27:59 +00:00
udo
dc5ccee805 Profile request execution phases 2026-07-16 15:12:13 +00:00
udo
62adc7a6a9 Reject empty time parse input 2026-07-16 14:29:28 +00:00
root
27d0258e1f stage request performance snapshots 2026-07-16 11:46:14 +00:00
udo
82aec81465 Refresh guest deadline after MySQL hostcalls 2026-07-15 09:18:35 +00:00
udo
ee022f0398 Ensure nested CLI components are fresh 2026-07-15 00:43:23 +00:00
udo
dafffb4ab6 label expected compile timing probe 2026-07-14 23:38:20 +00:00
udo
6b18587581 scope nested component props without deep copies 2026-07-14 22:41:55 +00:00
udo
0bcf9a676e flush runtime diagnostics promptly 2026-07-14 22:15:13 +00:00
udo
8c3f8c5e01 fix persisted unit failure identity 2026-07-14 21:44:31 +00:00
udo
d33d5e80ba fix canonical component artifact identity 2026-07-14 21:32:29 +00:00
udo
b6c2b8b08d docs: note installed gate site root 2026-07-14 08:46:01 +00:00
udo
306420e4d3 test: derive installed site directory for gates 2026-07-14 08:44:09 +00:00
udo
d7f4749f9f test: label expected source read failures 2026-07-14 06:23:50 +00:00
udo
afb17fc334 test: retry transient doc gate reads 2026-07-14 04:34:52 +00:00
udo
2a1400b78c test: run CLI gates by group 2026-07-14 04:04:55 +00:00
udo
9d707a1c04 Document source read failures 2026-07-14 02:52:36 +00:00
udo
c1949e7430 Label unreadable source probes distinctly 2026-07-14 02:48:24 +00:00
udo
8eec28a639 Clarify CSRF field raw rendering 2026-07-14 00:04:43 +00:00
udo
7fabccd90a Add CSRF hidden field helper 2026-07-13 23:52:49 +00:00
udo
096138d6a1 Fix wasm compile source race 2026-07-13 22:47:48 +00:00
udo
36ea0cd2a5 test: wait for dependency lock fixture readiness 2026-07-13 22:21:18 +00:00
root
8da6cad305 fix: retain resolved component paths 2026-07-13 21:26:01 +00:00
udo
92baaa6299 fix: prevent stale mutation execution 2026-07-13 20:17:48 +00:00
udo
0187bb60cf docs: publish password hashing APIs 2026-07-13 19:47:06 +00:00
udo
4414972079 feat: add native scrypt password hashing 2026-07-13 19:35:38 +00:00
udo
a288b539c6 Reject unreadable UCE source units 2026-07-13 15:28:35 +00:00
udo
cd2fa163e6 keep CLI units current during rebuilds 2026-07-13 14:20:29 +00:00
udo
97ecc6edd6 keep web requests off rebuild path 2026-07-13 12:25:42 +00:00
udo
5195ebeb25 avoid request stalls during proactive rebuilds 2026-07-13 12:12:14 +00:00
udo
c1039f5094 Parse standard absolute URL paths 2026-07-13 09:31:45 +00:00
udo
fe88d4143e Keep warm wasm workers alive 2026-07-13 08:59:11 +00:00
udo
f7e9c5a4ec Exclude cold module loads from guest deadlines 2026-07-13 08:51:46 +00:00
udo
73a2671112 Invalidate warmed wasm modules precisely 2026-07-13 08:36:55 +00:00
udo
ada01e34f4 Cache loaded source signatures safely 2026-07-13 08:07:37 +00:00
udo
02ed75fc85 Invalidate units when loaded sources change 2026-07-13 07:42:52 +00:00
udo
d857930ceb Expose MySQL affected row counts 2026-07-13 07:10:03 +00:00
root
7281302287 Keep MySQL connect available to wasm units 2026-07-13 06:24:59 +00:00
root
b1dfe6c807 Reuse MySQL connections within requests 2026-07-13 00:08:07 +00:00
udo
0ff42f357f Document request-scoped MySQL cleanup 2026-07-08 18:44:07 +00:00
udo
6bdccacf02 fix cli unit document root resolution 2026-07-06 20:35:09 +00:00
udo
6f446c4aef hide internal csrf session key helper 2026-07-06 10:08:44 +00:00
udo
b8c2efe976 add native csrf helpers 2026-07-06 09:58:25 +00:00
udo
125a24e2f9 redact sensitive runtime config logging 2026-07-06 08:03:09 +00:00
udo
a6cb1c0390 fix wasm session and mysql placeholder state 2026-07-06 07:54:01 +00:00
root
991a0f62b4 harden runtime config and docs 2026-06-27 20:58:07 +00:00
udo
c148c1b36b test: allow skipping nested local service probes 2026-06-22 02:39:36 +00:00
udo
30fcfce103 test: include response tails in site failures 2026-06-22 01:31:44 +00:00
udo
41b034739b test: use explicit failed-count checks 2026-06-22 01:14:07 +00:00
udo
bd007231de test: distinguish case failures from summary labels 2026-06-22 00:56:46 +00:00
udo
7cf930b4a8 test: accept explicit zero-failure summaries 2026-06-22 00:39:12 +00:00
udo
4d17d80f9a test: build failed-count markers explicitly 2026-06-22 00:21:10 +00:00
udo
0d59ea4b1c test: rely on site summary failure counts 2026-06-21 23:54:01 +00:00
udo
a0d085859c test: stabilize cold local service probes 2026-06-21 23:37:01 +00:00
udo
9e70dde366 test: detect site case failures explicitly 2026-06-21 23:05:32 +00:00
udo
c73dbc8503 test: retry cold site suite probes 2026-06-21 22:42:02 +00:00
udo
1d6c882fb7 test: wait for complete local HTTP bodies 2026-06-21 22:24:12 +00:00
udo
245119f952 test: report site suite marker details 2026-06-21 22:07:28 +00:00
udo
e113215ffa test: wait for slow streamed test responses 2026-06-21 21:50:37 +00:00
udo
1088f6b9a5 test: tolerate cold unit browser responses 2026-06-21 21:21:39 +00:00
root
1770323a2a fix test runner repo path handling 2026-06-21 21:05:51 +00:00
root
4a6fc46ec8 docs: document runtime cache request param 2026-06-21 21:02:36 +00:00
root
c0f3c5507c fix doc example cache path 2026-06-21 20:59:01 +00:00
root
4eda629c8d cleanup, docs 2026-06-17 13:02:57 +00:00
root
b8b56cf3dd cleanup, docs 2026-06-16 23:02:45 +00:00
root
ea08d5f28b cleanup, docs 2026-06-16 12:21:31 +00:00
root
dff1959341 cleanup, error pages 2026-06-16 08:46:54 +00:00
root
f2a3503ac3 configurable trans-membrance hostcall blocklist 2026-06-16 01:13:06 +00:00
root
52cf266a5e docs 2026-06-15 21:46:31 +00:00
root
99cd92fb4a docs 2026-06-15 21:42:50 +00:00
380 changed files with 7325 additions and 3482 deletions

1
.gitignore vendored
View File

@ -47,6 +47,7 @@ __pycache__/
.ruff_cache/
.coverage
htmlcov/
site/doc/examples/_gen/
# Editor swap artifacts
*.swp

176
DOCS-TODO.md Normal file
View File

@ -0,0 +1,176 @@
# DOCS-TODO — `/doc` documentation overhaul
Briefing for the doc-system overhaul. Read this fully before touching anything.
The approved design and the two locked decisions are: **(1) examples are LIVE and
SELF-VERIFYING** — real UCE code executed at render time, showing source + actual
captured output, gated by the test suite; **(2) FULL SWEEP** of all 257 pages.
The docs live under `site/doc/`:
- `index.uce` — the renderer (index + per-page detail view).
- `lib/doc_page.h` — page parsing (`load_doc_page`) + label/kind helpers.
- `style.css` — theme + layout.
- `pages/*.txt` — 257 page source files (the corpus).
- `areas/*.txt` — area groupings; first line is the area title, following lines
are page slugs (or `>area` references) that populate the index sidebar.
- `search.uce`, `singlepage.uce` — search + single-page render.
## Why this work exists (the problems)
- **Title bug:** 15 pages carry a `:title` that literally repeats the raw filename,
so `0_StringList` renders as "0_StringList" instead of "StringList". The `:title`
override defeats the prefix-stripping label logic already in `doc_page.h`
(`doc_default_title` / `doc_index_label` / `doc_method_label`).
- **31 slop/stub pages:** the newest APIs (`file_*`, `crypto_equal`, `sha256*`,
`hmac*`, `random_bytes`, `http_request*`, `job_*`, `shell_spawn`) are throwaway
`# name` + one-line markdown with NO `:sig`/`:params`/`:see`/`:content`.
- **134 of 257 pages have no code example.** We want PHP-manual-style
code-to-output examples on EVERY page.
- **Sidebar overflow:** long monospace identifiers in the 280px index sidebar and
220px detail sidebar have no wrapping → they spill.
- **Relevancy:** pages lead with cross-membrane mechanics and "Related Concepts"
PHP/JS filler instead of usage; cross-references between related pages are missing.
## Principles for every page
1. **Usage first.** The opening `:content` sentence says what the function does and
when you'd reach for it — in plain terms. No membrane/ABI talk up top.
2. **A real example, always.** Every page has at least one `:example` whose output
is produced by actually running it. Examples are the primary teaching tool.
3. **Cross-link.** Every page's `:see` references its area (`>area`) and its closest
sibling APIs, so the reader is never left to search alone.
4. **Demote the drivel.** Cross-membrane behavior, lock semantics, and PHP/JS
equivalents are at most a short trailing note — never the body. Delete filler
that teaches nothing.
---
## Phase 1 — Infrastructure (do this FIRST, verify on host before any content)
### 1a. Title bug
- In `lib/doc_page.h` `load_doc_page`: when a `:title` value, trimmed, **equals the
page slug**, ignore it (leave `result.title` empty) so the renderer falls through
to `doc_default_title`, which already strips `0_/1_/2_/3_` prefixes and renders
`Class::method` for `2_*`.
- Strip the 15 redundant `:title` blocks from their pages (`0_StringList`, `cli_arg`,
`cli_input`, `list_filter`, `list_map`, `map`, `request_base_url`,
`request_query_path`, `request_query_route`, `request_script_url`,
`route_path_is_safe`, `route_path_normalize`, `route_path_sanitize`, `ucb_encode`,
`ucb_decode`).
- Fix garbage `:sig` lines on struct pages (e.g. `0_StringList`'s sig is literally
`0_StringList`): give struct pages a real one-line type summary or drop the sig box.
### 1b. Live example mechanism — EXACT SPEC
Primitives (already confirmed to exist): `unit_compile(path)`, `unit_render(path)`,
`ob_start()`, `ob_get_close()`. Relative paths in the doc unit resolve against
`site/doc/`.
- **Parse:** add `:example` handling to `load_doc_page``DocPage.examples`
(a list; multiple `:example` blocks per page allowed). Capture the raw body lines
verbatim (this is both the displayed source AND the executed code).
- **Materialize:** for each example, write the body wrapped in a minimal unit to a
**stable** path `examples/_gen/<slug>_<n>.uce`:
```
RENDER(Request& context)
{
<example body>
}
```
Write **only if the content changed** (compare to existing file) so the runtime's
compiled-unit cache stays warm. The `_gen` dir must be writable by the worker
(www-data) — created/`chown`ed once on the host (see Setup below).
- **Execute + capture:** `unit_compile("examples/_gen/<slug>_<n>.uce");` then
`ob_start(); unit_render("examples/_gen/<slug>_<n>.uce"); String out = ob_get_close();`
- **Render:** an "Example" `doc-section` showing the source block, then the captured
output beneath it under an "Output" label (PHP-manual style). If the example
traps or output is empty, **surface that visibly** (do not swallow it) — a broken
example must be obvious so the gate catches it.
- Examples must be **self-contained and deterministic** — no wall-clock/random output
shown as canonical (if a function is inherently non-deterministic, show a
representative call and describe the shape, or seed it). The gate asserts presence
of an output block + absence of an error marker, not an exact value, EXCEPT where
the value is deterministic.
### 1c. Sidebar + example CSS (`style.css`)
- Add `overflow-wrap: anywhere; word-break: break-word;` to `.category li a`,
`.func-item a`, and `.sidebar-card div`.
- Style the example Output block so it's visually distinct from the source (subtle
border + an "Output" label), reusing existing `--bg-code`/`--border` tokens.
### 1d. Format spec page
- Create `pages/3_Documentation format.txt` (an `info` page) documenting the canonical
page template below, so the format is self-describing inside the docs.
### Canonical page template
```
:sig
<one or more real C++ signatures, exactly as declared>
:params
<name> : <what it is>
return value : <what comes back>
:content
<Usage-first prose. What it does, when to use it. 25 tight sentences.>
:example
<runnable UCE code that print()s something illustrative>
:see
><area>
<closest sibling API slugs>
```
Optional short trailing note in `:content` for membrane/PHP-JS equivalence — one line,
not a section. No `:title` unless it differs from the derived label.
### Setup (host, one-time, before content work)
```sh
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && mkdir -p site/doc/examples/_gen && chown -R www-data:www-data site/doc/examples/_gen && chmod 775 site/doc/examples/_gen'
```
---
## Phase 2 — Content sweep (batched by area; one area = one reviewable batch)
For each area file in `site/doc/areas/` (string, sys, types, sqlite, mysql, memcache,
session, websocket, task, time, uri, regex, ob, markup, socket, noise, runtime), bring
every listed page to the canonical template:
1. **Convert the 31 stubs** to full structured pages.
2. **Add a live `:example`** to every page missing one (≈134), each verified to render
real output.
3. **Repair `:see`** — area link + sibling links on every page; demote membrane / PHP-JS
filler to a one-line note.
4. **Cull dead pages** — remove `concat.txt` ("Removed. Not a current API") and audit
internal-only helpers (e.g. `json_consume_space`) for removal; also remove culled
slugs from the relevant `areas/*.txt`.
Per-batch bar: every touched page renders cleanly, its example produces real output,
and the suite stays green.
---
## Phase 3 — Gate
- Add a `site/tests/cli_runner.uce` regression test that renders **every** `?p=` page
and asserts HTTP 200, no error/trap marker, and (for pages with `:example`) presence
of a non-error Output block.
- Full host gate (this is the ONLY place builds/tests run — the sshfs mount is
edit-only, no WASI SDK on the client):
```sh
ssh root@10.4.2.110 'cd /Code/uce.openfu.com/uce && bash scripts/build_core_wasm.sh && bash scripts/build_linux.sh && systemctl restart uce.service && sleep 3 && bash scripts/run_cli_tests.sh --include-wasm-kill'
```
Expect the current 91 to grow by the new doc gate(s), 0 failed.
## Hard rules (non-negotiable)
- **NEVER `git commit`/push/tag.** Not pi, not its subagents, not anyone. Leave a clean
working tree and report.
- **Always run the real host gate** (`bash scripts/build_core_wasm.sh && bash
scripts/build_linux.sh && systemctl restart uce.service && ...`) and trust only its
output. `g++ -fsyntax-only` / client-side builds do NOT count — there is no WASI SDK
on the client and the doc unit must actually render.
- Sub-delegation model is exactly `gpt-5.3-codex-spark`.
- Verify the example mechanism end-to-end on the host BEFORE authoring content against
it — content written against an unproven mechanism is wasted.

View File

@ -88,7 +88,7 @@ Useful helpers for that data model include:
- `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()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
- `map()`, `filter()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code
Named component handlers are also supported:
@ -311,9 +311,13 @@ The example contains the filesystem and FastCGI settings:
BIN_DIRECTORY=/var/cache/uce/work
TMP_UPLOAD_PATH=/var/lib/uce/uploads
SESSION_PATH=/var/lib/uce/sessions
SESSION_COOKIE_SECURE=1
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
FCGI_SOCKET_MODE=0666
FCGI_PORT=9993
CLI_SOCKET_PATH=/run/uce/cli.sock
CLI_SOCKET_MODE=0600
PRECOMPILE_FILES_IN=
SITE_DIRECTORY=/var/www/html
@ -328,7 +332,7 @@ For nginx deployments, the most important setting is:
- `FCGI_SOCKET_PATH=/run/uce/fastcgi.sock`
That is the Unix socket nginx should use for normal `.uce` requests.
That is the Unix socket nginx should use for normal `.uce` requests. `CLI_SOCKET_PATH` is for local admin/test execution through `scripts/uce-cli`; keep `CLI_SOCKET_MODE=0600` unless a trusted Unix group explicitly needs access.
`FCGI_PORT` is optional if nginx is talking to the Unix socket. Leave it set if you also want a TCP FastCGI listener, or remove it if you want the socket to be the only FastCGI entry point.
@ -350,6 +354,7 @@ Recommended deployment notes:
- keep `HTTP_PORT` bound to localhost only at the firewall or by network policy; nginx should be the public entry point
- keep `BIN_DIRECTORY`, `TMP_UPLOAD_PATH`, and `SESSION_PATH` on writable local storage
- use `SESSION_COOKIE_SECURE=1` for HTTPS-only deployments; leave it `0` only for local/plain-HTTP development
- after editing `/etc/uce/settings.cfg`, restart `uce.service`
### 4. Install and enable the systemd service
@ -362,8 +367,8 @@ scripts/systemd/manage-uce-service.sh setup
That script:
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist
- installs `scripts/systemd/uce.service` as `/etc/systemd/system/uce.service`, rewriting the repository-root path in the unit to the checkout you ran it from
- installs `etc/uce/settings.cfg` to `/etc/uce/settings.cfg` if it does not already exist, likewise rewriting checkout-root paths
- reloads systemd
- enables the service at boot
- starts the runtime immediately

61
docs/doc-overhaul.md Normal file
View File

@ -0,0 +1,61 @@
# /doc Documentation Overhaul
## Objective
Implement the DOCS-TODO.md overhaul for the UCE `/doc` system on the sshfs source-of-truth tree, verifying builds/tests only on `root@10.4.2.110:/Code/uce.openfu.com/uce`.
## Success Criteria
- [x] Phase 1 infrastructure complete: title bug, live examples, CSS, format spec page.
- [x] Example mechanism verified end-to-end on host before broad content authoring.
- [x] Phase 2 example coverage sweep: all doc pages now have `:example`; filler/code-fence cleanup applied, resource-bound fences only.
- [x] Phase 3 doc rendering gate added.
- [x] Current host gate passes with 0 failed.
## Current State
- Status: verifying; Phase 1 + gate complete, StringList conversion complete, Phase 2 example/filler sweep complete.
- Last updated: 2026-06-16
- Source of truth: `/root/mount_ssh/uce-dev-root-htdocs-uce`
- Runtime/live target: `root@10.4.2.110:/Code/uce.openfu.com/uce`
## Goal Tree
Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` superseded
- [x] G1: Phase 1 infrastructure
- Done when: live examples render source/output on the host and title/sidebar/spec fixes are in place.
- Verify: host build/restart/curl spot check.
- [x] G1.1: Fix title override behavior and redundant title blocks.
- [x] G1.2: Add `:example` parser, materializer, executor, renderer.
- [x] G1.3: Add sidebar/example CSS.
- [x] G1.4: Add documentation format page.
- [x] G1.5: End-to-end host verification with one page example.
- [x] G2: Phase 2 content sweep by area
- Done when: all 257 pages match canonical template and examples render.
- Verify: area batch curls and host gate stays green.
- [x] G3: Phase 3 CLI doc gate
- Done when: CLI runner checks every doc page and examples.
- Verify: full host gate passes.
## Execution Queue
1. G2 area batching/delegation now that the mechanism is proven.
2. Keep the host suite green after each area batch.
## Decisions
- 2026-06-16: Follow DOCS-TODO.md exactly; no git commit/push/tag.
## Evidence and Verification Log
- 2026-06-16: Read `DOCS-TODO.md`, `site/doc/lib/doc_page.h`, `site/doc/index.uce`, `site/tests/cli_runner.uce`.
- 2026-06-16: Host setup/build/restart/curl for `2_DValue_filter` showed source and captured output `Ada`.
- 2026-06-16: Full host gate passed: `Summary: 349 passed, 0 failed, 0 skipped`.
- 2026-06-16: Converted retiring `list_unique/list_sort/list_some/list_every/list_find` free functions to `StringList` methods, updated callers and method docs; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
- 2026-06-16: Added live examples across types, string, regex, time, markup, noise, uri, and session area pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
- 2026-06-16: Added `:example` blocks to all remaining doc pages, removed full PHP/JS Related Concepts filler, removed duplicate content code fences except resource-bound pages; full host gate passed: `Summary: 353 passed, 0 failed, 0 skipped`.
## Change Log
- 2026-06-16: Created initial goal tree.

View File

@ -11,20 +11,20 @@ A typical deployment has four pieces:
3. `uce.service`, a systemd service that builds/starts/restarts the runtime.
4. nginx or Apache as the public HTTP server.
Recommended filesystem layout for a source checkout:
Recommended filesystem layout for a source checkout (replace paths as needed):
```text
/opt/uce/ UCE repository/runtime root
/var/www/html/ public web root served by nginx/Apache
/etc/uce/settings.cfg runtime configuration
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
/run/uce/cli.sock local CLI/admin/test socket
/var/cache/uce/work generated source, wasm modules, caches
/var/lib/uce/uploads multipart upload scratch space
/var/lib/uce/sessions session files
/path/to/uce-root UCE repository/runtime root (for example `/opt/uce` or `/Code/uce.openfu.com/uce`)
/var/www/html/ public web root served by nginx/Apache
/etc/uce/settings.cfg runtime configuration
/run/uce/fastcgi.sock FastCGI socket used by nginx/Apache
/run/uce/cli.sock local CLI/admin/test socket
/var/cache/uce/work generated source, wasm modules, caches
/var/lib/uce/uploads multipart upload scratch space
/var/lib/uce/sessions session files
```
For packaged installs, the runtime may live under `/usr/lib/uce` instead of `/opt/uce`. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
For packaged installs, the runtime may live under `/usr/lib/uce` instead of your checkout root. Keep the public web root at `/var/www/html` or another normal web-root path, not under the runtime source tree.
## Build requirements
@ -32,10 +32,10 @@ On Debian/Ubuntu-like systems, install the distro packages first:
```bash
apt update
apt install -y clang build-essential libpcre2-dev mariadb-client libmariadb-dev curl rsync ca-certificates
apt install -y clang build-essential libpcre2-dev libssl-dev mariadb-client libmariadb-dev curl rsync ca-certificates
```
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans.
UCE also requires two non-vendored dependencies. WASI SDK is load-bearing at runtime because UCE compiles units on demand during requests and during proactive startup scans. The `curl` binary is also a pinned runtime package dependency: `http_request()` and `http_request_async()` execute it directly with an explicit argument vector for TLS-capable outbound HTTP.
- **Wasmtime C API / C++ headers** at `/opt/wasmtime` by default. `scripts/build_linux.sh` expects:
- `/opt/wasmtime/include/wasmtime.hh`
@ -62,10 +62,11 @@ apt install -y nginx
apt install -y apache2
```
Build UCE from the repository root:
Build UCE from your repository root:
```bash
cd /opt/uce
repo_root=/path/to/uce-root
cd "$repo_root"
bash scripts/build_core_wasm.sh
bash scripts/build_linux.sh
```
@ -104,7 +105,8 @@ The expected directories are:
Install the WASI SDK:
```bash
cd /opt/uce
repo_root=/path/to/uce-root
cd "$repo_root"
scripts/install_wasi_sdk.sh
scripts/install_wasi_sdk.sh --check-only
```
@ -163,7 +165,7 @@ systemctl restart uce.service
## Runtime configuration
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths if your runtime is not under `/opt/uce`. Replace any checkout-specific paths such as `WASM_CORE_PATH` with `/opt/uce/bin/wasm/core.wasm` or your actual runtime path.
Create `/etc/uce/settings.cfg` from `etc/uce/settings.cfg` and adjust paths to your checkout root. Replace checkout-specific settings such as `WASM_CORE_PATH` with `<UCE_REPO_ROOT>/bin/wasm/core.wasm` or leave it relative to `COMPILER_SYS_PATH` as appropriate.
Minimum useful settings:
@ -171,9 +173,12 @@ Minimum useful settings:
BIN_DIRECTORY=/var/cache/uce/work
TMP_UPLOAD_PATH=/var/lib/uce/uploads
SESSION_PATH=/var/lib/uce/sessions
SESSION_COOKIE_SECURE=1
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
FCGI_SOCKET_MODE=0666
CLI_SOCKET_PATH=/run/uce/cli.sock
CLI_SOCKET_MODE=0600
SITE_DIRECTORY=/var/www/html
HTTP_DOCUMENT_ROOT=/var/www/html
@ -183,7 +188,7 @@ PROACTIVE_COMPILE_CHECK_INTERVAL=60
WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
WASM_BACKEND_VERBOSE=0
WASM_CORE_PATH=/opt/uce/bin/wasm/core.wasm
WASM_CORE_PATH=<UCE_REPO_ROOT>/bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
@ -193,17 +198,21 @@ MAX_MEMORY=16777216
SESSION_TIME=2592000
HTTP_PORT=8080
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
```
Important settings:
- `FCGI_SOCKET_PATH` is the Unix socket used for normal `.uce` requests. Set it explicitly and keep this value and the web-server `fastcgi_pass` path identical. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units.
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory.
- `CLI_SOCKET_PATH` is a local HTTP-over-Unix socket used by `scripts/uce-cli` and test/admin units. Keep it private (`CLI_SOCKET_MODE=0600`) unless you intentionally delegate admin/test execution to a trusted Unix group (`0660`).
- `FCGI_SOCKET_MODE` and `CLI_SOCKET_MODE` are octal permission modes applied after socket bind. Prefer tightening `FCGI_SOCKET_MODE` to `0660` when nginx/Apache can share a trusted group with the UCE worker.
- `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory. Installed regression gate scripts derive their temporary test root from this setting unless `UCE_TEST_SITE_DIRECTORY` is explicitly provided.
- `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache.
- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches.
- `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime.
- `SESSION_COOKIE_SECURE=1` adds the `Secure` attribute to UCE-managed session cookies and should be used for HTTPS-only deployments. Leave it `0` only for local/plain-HTTP development.
- `HTTP_PORT` is the built-in HTTP/WebSocket listener used for WebSocket upgrade traffic and direct local probes. Bind/firewall it for local access only; nginx/Apache should be the public entry point.
- `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` controls how long a forwarded WS message can remain queued in the broker before being dropped (default `30`). Set to `0` to disable the timeout.
- `WASM_COMPILE_SCRIPT` must point to `scripts/compile_wasm_unit` unless you provide an equivalent compiler. Relative paths are resolved from the runtime root/`COMPILER_SYS_PATH`. That script calls `scripts/check_unit_wasm.py` after linking each unit and uses the pinned WASI SDK on every deployment host.
- `WASM_CORE_PATH` must point at the built `core.wasm` file.
@ -218,11 +227,12 @@ systemctl restart uce.service
For source-checkout deployments, install the provided service helper:
```bash
cd /opt/uce
repo_root=/path/to/uce-root
cd "$repo_root"
scripts/systemd/manage-uce-service.sh setup
```
That helper installs `scripts/systemd/uce.service`, creates runtime directories, enables the service, and starts it.
That helper installs `scripts/systemd/uce.service` from your checked-out tree, rewrites the checked-out repository path into the installed unit and first-time config, creates runtime directories, enables the service, and starts it. Use a custom unit only if you need a nonstandard runtime layout.
Useful commands:
@ -232,7 +242,7 @@ scripts/systemd/manage-uce-service.sh restart
scripts/systemd/manage-uce-service.sh logs 200
```
Equivalent manual systemd service for a source checkout:
Equivalent manual systemd service for a source checkout (`<UCE_REPO>` = checkout root):
```ini
[Unit]
@ -242,14 +252,14 @@ Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/uce
WorkingDirectory=<UCE_REPO>
RuntimeDirectory=uce
StateDirectory=uce
CacheDirectory=uce
ExecStartPre=/usr/bin/mkdir -p /var/cache/uce/work /var/lib/uce/uploads /var/lib/uce/sessions
ExecStartPre=/usr/bin/rm -f /run/uce/fastcgi.sock
ExecStartPre=/usr/bin/bash /opt/uce/scripts/build_linux.sh
ExecStart=/opt/uce/bin/uce_fastcgi.linux.bin
ExecStartPre=/usr/bin/bash <UCE_REPO>/scripts/build_linux.sh
ExecStart=<UCE_REPO>/bin/uce_fastcgi.linux.bin
ExecStopPost=/usr/bin/rm -f /run/uce/fastcgi.sock
Restart=always
RestartSec=2
@ -582,7 +592,8 @@ curl -i http://127.0.0.1/examples/uce-starter/ -H 'Host: example.com'
Check the local CLI path:
```bash
cd /opt/uce
repo_root=/path/to/uce-root
cd "$repo_root"
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/run_cli_tests.sh
```
@ -610,10 +621,26 @@ sock.close()
PY
```
## Password hashing
Use the native password API for application credentials:
```cpp
String encoded = password_hash(password);
if(encoded == "")
// fail the write; native hashing did not complete
bool valid = password_verify(candidate, encoded);
if(valid && password_needs_rehash(encoded))
encoded = password_hash(candidate);
```
`password_hash()` returns a self-contained `$uce$scrypt$...` encoding with a random 16-byte salt and the bounded scrypt parameters `N=65536`, `r=8`, `p=1`. `password_verify()` accepts only structurally valid encodings with bounded cost parameters and compares the derived key in constant time. `password_needs_rehash()` reports malformed, legacy, or non-current parameters so applications can upgrade a credential after a successful legacy verification. Treat an empty hash as an operational failure and never store it. Application-level password length policy, rate limiting, and legacy-format verification remain the application's responsibility.
## Operational footguns
- Keep the FastCGI socket path consistent: `FCGI_SOCKET_PATH` and the web-server `fastcgi_pass` must match exactly. The reference config uses `/run/uce/fastcgi.sock`; if you choose `/run/uce.sock`, use it in both places.
- Keep the public web root separate from the runtime source tree. The examples use `/opt/uce` for runtime files and `/var/www/html` for public files.
- Keep the public web root separate from the runtime source tree. Replace `/opt/uce` with your actual checkout path in local examples (for example `/opt/uce` or `/Code/uce.openfu.com/uce`), and keep public files under `/var/www/html`.
- Set `HTTP_DOCUMENT_ROOT` when the web root is outside the runtime working directory. The built-in HTTP/WebSocket listener resolves upgrade paths from this setting.
- Do not expose `CLI_SOCKET_PATH` or `HTTP_PORT` as public entry points. The public path should be nginx/Apache.
- Do not trust `Script-Filename` request headers from direct HTTP clients. The built-in HTTP listener resolves from `HTTP_DOCUMENT_ROOT` and rejects `..` path segments.

View File

@ -26,10 +26,10 @@ gets invoked*.
```
┌────────────────────────────┐
nginx ──FastCGI──► worker pool (N processes) │ /run/uce.sock
nginx ──FastCGI──► worker pool (N processes) │ $FCGI_SOCKET_PATH (example `/run/uce/fastcgi.sock`)
(port 80 etc.) │ uniform unit renderers │ (FastCGI + CLI)
└─────────────▲──────────────┘
│ forward render (FastCGI, uce.sock)
│ forward render (FastCGI, FCGI_SOCKET_PATH)
browser ──raw HTTP / WS──► ┌────────┴─────────┐
(HTTP_PORT 8080) │ WS broker │ owns HTTP_PORT + every
@ -48,14 +48,14 @@ gets invoked*.
| Process | Owns | Renders units? | Source |
|---|---|---|---|
| **Parent** | nothing; supervises children | no | `main()`, `init_base_process()` |
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (`/run/uce.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
| **Worker** (×`WORKER_COUNT`) | `FCGI_SOCKET_PATH` (configured socket path; example `/run/uce/fastcgi.sock`) + `CLI_SOCKET_PATH` | **yes** — the only processes that run wasm | `listen_for_connections()` |
| **WS broker** (×1) | `HTTP_PORT` + every live WS connection + `WS_BROKER_SOCKET_PATH` | no — forwards to the pool | `run_ws_broker()` |
| **serve_http dispatcher** (×bind) | one custom-server bind address | no — forwards to the pool | `custom_server_http_dispatcher_loop()` |
| **Proactive compiler** | nothing; pre-compiles units | no | `run_proactive_compiler()` |
**only workers instantiate Wasmtime and run unit code.**
Every connection-owning process (broker, serve_http dispatcher) forwards the
request invocation back to a worker over `/run/uce.sock` using the minimal
request invocation back to a worker via `FCGI_SOCKET_PATH` using the minimal
FastCGI client in [`src/lib/fcgi_forward.h`](../src/lib/fcgi_forward.h). This is
forced by Wasmtime: an `Engine`/`Store` cannot be safely re-created across
`fork()`, and the brokers fork from the parent that already touched the
@ -85,6 +85,14 @@ the boundary).
See [`docs/wasm-phase1-dvalue-abi.md`](wasm-phase1-dvalue-abi.md) for the wire
format details.
Decoder robustness note: UCEB input is untrusted across the wasm membrane. The
UCEB decoder must reject malformed magic/version/varint/length/trailing-data
inputs and excessive nesting with explicit errors, not native or wasm traps. Its
current hard nesting cap is intentionally low (64 levels) to stay well below the
guest stack limit. JSON decoding follows the same rule for malformed strings and
unicode escapes: validate bounds before every read and return an empty/partial
`DValue` rather than reading beyond the input.
---
## 3. Units, handlers, and export naming
@ -120,6 +128,38 @@ without instantiating it.
backend is initialized and the requested artifact/handler is currently
available. If an artifact is cold or stale, dispatch compiles it on demand via
`get_shared_unit()` and rechecks. There is no native unit-execution fallback.
Workers identify cached unit artifacts by nanosecond mtime, ctime, and size.
Whole-second mtime alone is insufficient because a dependency-triggered rebuild
can replace a wasm artifact within the same second as its prior build.
Cold module compilation and deserialization are host work, so `load_unit()`
refreshes the epoch deadline before its first guest call. Otherwise a component
whose compilation outlasted the guest CPU budget would immediately trap in the
following allocator/relocation call even though no guest loop consumed it.
The proactive compiler and request workers coordinate through per-unit file
locks and a lock-protected demand-priority queue under `BIN_DIRECTORY`. Unit
compilation writes and validates a process-unique temporary wasm file, then
publishes it with an atomic rename. When proactive compilation is enabled,
read-only HTTP requests keep using the last complete artifact while requesting
that stale unit at the head of the compiler queue. Non-read requests never run
a stale entry artifact: they return `503 Service Unavailable` with
`Retry-After: 1`, allowing the client to retry after the priority rebuild.
CLI and explicit compile paths remain synchronous. A failed rebuild removes
availability and surfaces the compiler error rather than serving the old unit
indefinitely. This freshness contract includes components resolved lazily from
an otherwise-current CLI entry unit: if the proactive compiler already owns a
stale child's lock, the CLI request joins that compile and waits for the fresh
artifact. Read-only HTTP may use the child's last complete artifact instead.
Before preprocessing, the compiler verifies that the worker can actually read
the source. An unreadable path is reported as a source-read failure with a
persisted diagnostic; it never becomes an apparently valid empty side module or
a generic compiler error. Source signatures mark unreadable inputs, so correcting
access invalidates that failure and permits a normal retry without changing
signatures for ordinary readable files. The private CLI harness may label its
intentional unreadable-source regression request as an expected source-read
failure, keeping production log scans focused on real runtime failures while
preserving the failing compile result and diagnostic artifacts.
---
@ -129,7 +169,7 @@ Each request gets a fresh **workspace** — a per-request wasm instance tree wit
the membrane wired in. `wasm_worker_serve(worker, ctx, entry_unit, handler)`
(`src/wasm/worker.cpp`) is the single entry point for *every* mode:
1. Birth a workspace (CoW-snapshot-based where available).
1. Birth a request-scoped workspace with fresh per-request state. Current production workers instantiate a fresh workspace for each request; any future snapshot/CoW optimization must preserve that request-isolation contract.
2. Resolve `entry_unit` + `handler` to an export; components referenced at
runtime are resolved on demand via the `uce_host_component_resolve` hostcall
(`component_resolve()``__uce_<...>` slot), loading dependency modules
@ -144,6 +184,25 @@ suspend the native SIGSEGV/SIGILL recovery handler around the wasm call so that
Wasmtime's own trap signals are not escalated into a native fatal signal (see
`serve_via_wasm` in `handle_complete`).
Render workers are long-lived. The generic FastCGI transport retains a legacy
eight-connection recycle default, but `listen_for_connections()` disables it
for the UCE pool: recycling a healthy worker discards its Wasmtime engine and
module cache, adding a visible cold-start request. Request state remains bounded
by the fresh workspace and the normal per-request database/resource cleanup;
faulted workers still exit and are replaced by the parent.
### Task callbacks and workspace lifetime
`task()` and `task_repeat()` are fork-backed. The `uce_host_task_spawn` hostcall
captures the current `WasmWorkspace*`, but `src/lib/sys.cpp::task()` invokes the
captured callback only in the forked child, before the hostcall stack unwinds in
that child. The parent request may return and destroy its workspace; the child
is forked from the parent and keeps a private request-context copy of the per-request workspace. This means a delayed task callback can run after the spawning request
returns without dereferencing the parent's destroyed workspace. It is still a
callback into the inherited child workspace, not a fresh normal request
workspace; avoid adding host resources to `WasmWorkspace` that are invalid across
`fork()` unless task callback handling is changed to birth a fresh workspace.
---
## 5. Request dispatch (`handle_complete`)
@ -161,7 +220,7 @@ otherwise (page) → serve_via_wasm(entry_unit, "render")
The `UCE_*` params are set by whichever broker forwarded the request:
- **Page render**: FastCGI nginx → `/run/uce.sock` directly; no `UCE_*` flags → `render`.
- **Page render**: FastCGI nginx → `FCGI_SOCKET_PATH` directly; no `UCE_*` flags → `render`.
- **CLI**: the CLI socket sets `is_cli`.
- **serve_http**: the custom-server dispatcher sets `UCE_SERVE_HTTP=1` plus
`UCE_SERVE_HTTP_FUNCTION` and rewrites `SCRIPT_FILENAME` to the configured
@ -197,13 +256,15 @@ broker loop:
2. The message rides as `UCE_WS_MESSAGE` (base64) with an **empty STDIN body**
a non-empty STDIN makes the FastCGI transport flush a premature response
before `on_complete` ever runs.
3. Connect to `/run/uce.sock` (non-blocking) and queue the encoded request in
`ws_broker_outbound[fd]`.
3. Connect to `FCGI_SOCKET_PATH` (non-blocking) and queue the encoded request in
`ws_broker_outbound[fd]` with an enqueue timestamp.
`ws_broker_drain_outbound()` runs after every `process(50)` tick: it finishes
writing each queued request, then drains and discards the reply (the unit's
output comes back via the command socket, not this reply), closing the fd when
the worker closes its end.
the worker closes its end. If a forward remains pending beyond
`WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`), the broker drops and
closes it so a wedged worker cannot pin broker fds/memory indefinitely.
### 6.2 Outbound: `ws_*` commands flushed back to the broker
@ -211,8 +272,10 @@ Any unit code — not just WebSocket handlers — may call `ws_send` / `ws_send_
/ `ws_close`. In the workspace these **record dispatch commands** rather than
touching a socket (the workspace owns no connections); `wasm-core`'s `ws_*`
(`src/lib/sys.cpp`) append to `websocket_dispatch_commands`, and
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands` (plus
`ws_connection_state` if the handler mutated per-connection state).
`finish_response_meta` (`src/wasm/core.cpp`) emits them as `ws_commands`.
If the handler changed per-connection state, the core also emits
`ws_connection_state` even when no commands were emitted, and the native
backend flushes this state-only batch to the broker.
`wasm_backend_serve` (`src/wasm/backend.cpp`) flushes that batch at workspace
teardown — in **any** scenario, not just WS handlers — to the broker's command
@ -239,9 +302,10 @@ serve_http dispatcher uses, so there is no duplicated request-forwarding code.
handlers (it renders nothing, so it accepts every request straight through to
`on_complete`), wires `on_complete=ws_broker_complete` and
`on_websocket_message=ws_broker_ws_message`, listens on `HTTP_PORT` and the
command socket, and loops `process(50)` + `drain_outbound()`. The design is
**non-blocking outbound dispatch + async command-socket flush, all in the
broker's single epoll loop** — the broker never blocks on a worker.
command socket, and loops `process(50)` + `drain_outbound(timeout)`. The
`timeout` comes from `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` (default `30`). The
design is **non-blocking outbound dispatch + async command-socket flush, all in
the broker's single epoll loop** — the broker never blocks on a worker.
The parent respawns the broker if it dies (`ws_broker_alive` / `ensure_ws_broker`
in `main()`).
@ -284,10 +348,13 @@ header free-functions are `inline`. The wasm backend exposes only declarations
| Key | Default | Meaning |
|---|---|---|
| `WASM_BACKEND_VERBOSE` | `0` | Emit `X-UCE-Wasm-*` workspace timing headers (benchmark only). |
| `FCGI_SOCKET_PATH` | `/run/uce.sock` | Worker pool FastCGI socket (brokers forward here). |
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI socket. |
| `FCGI_SOCKET_PATH` | runtime-configured (`/run/uce/fastcgi.sock` in this doc) | Worker pool FastCGI socket (brokers forward here). |
| `CLI_SOCKET_PATH` | `/run/uce/cli.sock` | Worker CLI/admin socket. Keep private; reference `CLI_SOCKET_MODE` is `0600`. |
| `FCGI_SOCKET_MODE` | `0666` | Permission mode applied to `FCGI_SOCKET_PATH` after bind; set tighter if nginx/Apache can use a trusted group. |
| `CLI_SOCKET_MODE` | `0600` | Permission mode applied to `CLI_SOCKET_PATH`; set `0660` only for a trusted admin group. |
| `HTTP_PORT` | `8080` | Raw HTTP + WebSocket port — owned by the WS broker. |
| `WS_BROKER_SOCKET_PATH` | `/run/uce/ws-broker.sock` | Broker command socket for `ws_*` flushes. |
| `WS_BROKER_OUTBOUND_TIMEOUT_SECONDS` | `30` | Max lifetime in seconds for queued WS broker forwards before drop. |
| `WORKER_COUNT` | `4` | Number of uniform worker processes. |
---
@ -295,11 +362,65 @@ header free-functions are `inline`. The wasm backend exposes only declarations
## 10. Testing
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages.
in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages and
`scripts/test_dependency_invalidation.sh`. The shell helper runs the CLI suite
as named groups (`demo`, `http`, `site`, `doc-gate`, `security`,
`task-lifetime`, `starter`, `tcp`, and optional `wasm-kill`) so a slow or
failing group is visible before the rest of the gate runs. `doc-gate` can take
several minutes on a cold runtime because it compiles generated documentation
examples; `UCE_CLI_TEST_TIMEOUT` controls the per-group curl timeout and
defaults to 900 seconds. Individual doc-page checks retry transient empty
frontend reads, but still fail persistent status, marker, compile, runtime, or
placeholder-example errors. The dependency-invalidation gate changes a transitive
`#load`, then replaces a warmed worker artifact while preserving its
whole-second mtime to prove both compiler and worker caches invalidate it. It
also rejects an unreadable unit without publishing a wasm artifact, restores
its permissions and proves the next CLI request compiles it, then sends 48
requests and asserts the observed worker PID set does not exceed
`WORKER_COUNT`, guarding against accidental reintroduction of request-count
recycling. It also races a source edit against an active unit compile after
the generated C++ snapshot has been written. The compiler must either retry
and serve the post-edit source or fail closed; it must never stamp current
source metadata onto wasm produced from an older snapshot.
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately
cold component that exceeds the development epoch window and proves the
parent request still renders it. The focused shell gates create temporary
`.uce` units under `UCE_TEST_SITE_DIRECTORY`, or under the installed
`SITE_DIRECTORY` from `/etc/uce/settings.cfg` when no override is supplied.
The dependency-invalidation gate also holds parent and child compile locks
across a transitive source edit, proves a warmed HTTP request returns the
last atomically published result without waiting, proves a POST returns a
prompt retryable 503 without executing its old mutation handler, and verifies
demand-priority convergence. A CLI request still waits and returns only the
current dependency result after rebuild completion.
The component page also calls a parent once without its optional relative
child and again with that child activated. This guards the component-slot
cache invariant that a cache hit must restore both the function-table slot
and the resolved unit path; otherwise nested relative targets resolve from
the caller after a repeated parent invocation. Resolved component paths are
canonical absolute paths: equivalent spellings containing `.` or `..` must
share one source, compile, artifact, and module-cache identity. The compiler
independently enforces the same canonical identity, and persisted failures
are reused only when their recorded source path matches the current request.
`scripts/test_nested_component_props.sh` passes a large prop tree through an
outer component, invokes 300 nested components, and verifies both inner and
caller props are restored. Its warm-request ceiling guards the request-scope
invariant that entering a nested component swaps the active prop trees in
constant time instead of deep-copying the outer tree for every child.
`scripts/test_mysql_epoch_refresh.sh` spends more than half of the configured
guest epoch on each side of a real MySQL query. It proves the blocking
database hostcall refreshes the guest deadline so cumulative request work is
allowed while a CPU loop without intervening host work still traps.
- **Log timeliness**: the base process line-buffers stdout before forking
workers, the proactive compiler, and the WebSocket broker. This keeps each
newline-terminated diagnostic attached to its actual journal time instead of
releasing an old block-buffered failure during unrelated later traffic. The
private CLI timing gate labels its deliberate invalid-source request as
`UCE expected compile error`; public and unmarked CLI compilation failures
retain the ordinary `UCE compile error` operator signal.
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
broker → worker → broker → client chain across process boundaries.

View File

@ -5,16 +5,24 @@ SESSION_PATH=/var/lib/uce/sessions
# LISTEN ON SOCKETS
FCGI_SOCKET_PATH=/run/uce/fastcgi.sock
# FastCGI is normally consumed by the local web server user/group.
FCGI_SOCKET_MODE=0666
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
# Keep the CLI/admin socket private by default; set 0660 only for a trusted group.
CLI_SOCKET_MODE=0600
# Built-in HTTP/WebSocket listener used for WebSocket Upgrade requests.
# Keep this behind nginx/Apache on localhost or firewall it from public access.
HTTP_PORT=8080
# After this many seconds, an in-flight WS broker render-forward that is still
# not completed (including stalled writes/reads) is dropped.
WS_BROKER_OUTBOUND_TIMEOUT_SECONDS=30
# OPTIONAL PROACTIVE COMPILE ROOT
# Leave empty to scan SITE_DIRECTORY relative to the runtime root.
PRECOMPILE_FILES_IN=
@ -30,7 +38,7 @@ WASM_COMPILE_SCRIPT=scripts/compile_wasm_unit
# WASM RUNTIME SETTINGS. Unit execution is always routed through wasm.
WASM_BACKEND_VERBOSE=0
WASM_CORE_PATH=/Code/uce.openfu.com/uce/bin/wasm/core.wasm
WASM_CORE_PATH=bin/wasm/core.wasm
WASM_MEMORY_LIMIT_BYTES=536870912
WASM_EPOCH_DEADLINE_TICKS=200
WASM_EPOCH_PERIOD_MS=50
@ -81,3 +89,14 @@ ARCHIVE_MAX_ZIP_ENTRIES=4096
# LIFETIME OF SESSION COOKIES IN SECONDS
SESSION_TIME=2592000
# Set to 1 behind HTTPS-only frontends to add Secure to session cookies.
SESSION_COOKIE_SECURE=0
# DEVELOPER-DEFINED ERROR PAGES (see /doc/index.uce?p=error_pages and
# /doc/index.uce?p=3_blocked_functions). The unit receives the error details in
# context.call["error"]; comment out for the plain-text fallback instead.
page_runtime_error=site/errors/runtime-error.uce
# Comma-separated uce_host_* names to disable per server policy (empty = none).
# A unit calling a blocked hostcall fails into the error page above.
UCE_HOSTCALL_BLOCKLIST=

View File

@ -15,35 +15,41 @@ DOC_DIR = ROOT / "site" / "doc" / "pages"
# name, needs_doc, status. status: public | internal | integration
PUBLIC_APIS = [
("shell_exec", True, "public"), ("shell_escape", True, "public"),
("http_request", True, "public"), ("http_request_async", True, "public"),
("shell_exec", True, "public"), ("shell_escape", True, "public"), ("shell_spawn", True, "public"),
("job_status", True, "public"), ("job_result", True, "public"), ("job_await", True, "public"), ("job_cancel", True, "public"),
("basename", True, "public"), ("dirname", True, "public"), ("path_join", True, "public"),
("path_real", True, "public"), ("path_is_within", True, "public"),
("file_open_locked", True, "public"), ("file_close_locked", True, "public"),
("file_release_process_locks", True, "public"), ("file_get_contents_locked_fd", True, "public"),
("file_put_contents_locked_fd", True, "public"), ("file_get_contents", True, "public"),
("file_put_contents", True, "public"), ("file_append_contents", False, "public"),
("file_get_contents", True, "public"),
("file_put_contents", True, "public"), ("file_append", True, "public"),
("cwd_get", True, "public"), ("cwd_set", True, "public"), ("process_start_directory", True, "public"),
("file_mtime", True, "public"), ("file_unlink", True, "public"), ("expand_path", True, "public"),
("ls", True, "public"), ("config_map_u64", True, "public"), ("config_map_f64", True, "public"),
("config_bool_value", True, "public"), ("config_map_bool", True, "public"),
("config_u64", True, "public"), ("config_f64", True, "public"), ("config_bool", True, "public"),
("ls", True, "public"), ("to_u64", True, "public"), ("to_s64", True, "public"),
("to_f64", True, "public"), ("to_bool", True, "public"),
("request_perf", True, "public"), ("time_format_local", True, "public"),
("time_format_relative", True, "public"), ("time_parse", True, "public"),
("backtrace_frames_string", False, "public"), ("capture_backtrace_string", False, "public"),
("backtrace_get_frames", False, "public"), ("backtrace_capture", False, "public"),
("signal_name", False, "public"), ("memcache_escape_key", True, "public"),
("memcache_escape_keys", True, "public"), ("memcache_command", True, "public"),
("memcache_get_multiple", True, "public"), ("runtime_safe_key", True, "public"),
("float_val", True, "public"), ("nibble", True, "public"), ("json_consume_space", False, "public"),
("array_merge", True, "public"), ("safe_name", True, "public"), ("ascii_safe_name", True, "public"),
("to_json", False, "public"), ("remove", False, "public"), ("clear", False, "public"),
("gen_sha1", True, "public"), ("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
("gen_sha1", True, "public"), ("sha256", True, "public"), ("sha256_hex", True, "public"),
("hmac_sha256", True, "public"), ("hmac_sha256_hex", True, "public"), ("random_bytes", True, "public"),
("crypto_equal", True, "public"), ("password_hash", True, "public"),
("password_verify", True, "public"), ("password_needs_rehash", True, "public"),
("gen_noise32", True, "public"), ("gen_noise64", True, "public"),
("gen_noise01", True, "public"), ("gen_int", True, "public"), ("gen_float", True, "public"),
("draw_int", True, "public"), ("draw_float", True, "public"),
("encode_query", True, "public"), ("request_script_url", True, "public"),
("encode_query", True, "public"), ("csrf_token", True, "public"),
("csrf_valid", True, "public"), ("csrf_rotate", True, "public"),
("request_script_url", True, "public"),
("request_base_url", True, "public"), ("request_route_from_raw_path", True, "public"),
("cli_arg", True, "public"), ("unit_compile", True, "public"),
("cleanup_sqlite_connections", False, "internal"), ("cleanup_mysql_connections", False, "internal"),
("mysql_connect", True, "integration"), ("mysql_query", True, "integration"),
("mysql_affected_rows", True, "integration"),
]
REMOVED_APIS = ["unit_load", "concat"]

View File

@ -21,7 +21,7 @@ WASMTIME_HOME=${WASMTIME_HOME:-/opt/wasmtime}
WASM_FLAGS="-I$WASMTIME_HOME/include"
WASM_LIBS="-L$WASMTIME_HOME/lib -Wl,-rpath,$WASMTIME_HOME/lib -lwasmtime"
LIBS="-ldl -lm -lpthread -lpcre2-8 `mysql_config --cflags --libs` $WASM_LIBS"
LIBS="-ldl -lm -lpthread -lpcre2-8 -lcrypto `mysql_config --cflags --libs` $WASM_LIBS"
SRCFLAGS="-D EXEC_NAME=\"$GF\" -D PLATFORM_NAME=\"linux\""
# The runtime is split into separately-compiled objects so an edit to one

View File

@ -15,6 +15,7 @@ ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$"
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
COMMON_FLAGS=(
@ -44,6 +45,7 @@ PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
mkdir -p "$DEST_DIR" >/dev/null 2>&1
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$WASM_TMP"' EXIT
build_pch_if_needed() {
if [ "$PCH_ENABLED" = "0" ]; then
@ -80,7 +82,7 @@ fi
"$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \
--Bsymbolic \
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \
"$OBJ_FN" -o "$WASM_TMP" \
--export-if-defined=__uce_set_current_request \
--export-if-defined=__uce_render \
--export-if-defined=__uce_component \
@ -90,8 +92,9 @@ fi
--export-if-defined=__uce_once \
--export-if-defined=__uce_init
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN"
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$WASM_TMP"
python3 scripts/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
mv "$WASM_TMP" "$DEST_DIR/$WASM_FN"
rm -f "$OBJ_FN" "$ABI_TMP"

View File

@ -59,7 +59,12 @@ verify_tree() {
return 1
fi
done
if ! command -v curl >/dev/null 2>&1; then
echo "Missing runtime dependency: curl (required by UCE http_request/http_request_async)" >&2
return 1
fi
"$root/bin/clang++" --version | head -n 1
curl --version | head -n 1
}
if [[ $check_only -eq 1 ]]; then

View File

@ -3,6 +3,7 @@ set -euo pipefail
cd "$(dirname "$0")/.."
socket_path="${UCE_CLI_SOCKET:-/run/uce/cli.sock}"
curl_timeout="${UCE_CLI_TEST_TIMEOUT:-900}"
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
@ -11,6 +12,7 @@ if [[ -z "${UCE_CLI_SOCKET:-}" && -r /etc/uce/settings.cfg ]]; then
fi
include_kill=0
skip_local_service_pages=0
action="run"
while [[ $# -gt 0 ]]; do
case "$1" in
@ -18,15 +20,22 @@ while [[ $# -gt 0 ]]; do
include_kill=1
shift
;;
--skip-local-service-pages)
skip_local_service_pages=1
shift
;;
--list)
action="list"
shift
;;
-h|--help)
cat <<'USAGE'
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--list]
Usage: scripts/run_cli_tests.sh [--include-wasm-kill] [--skip-local-service-pages] [--list]
Runs the UCE unit-based test suite through the runtime CLI socket.
Environment:
UCE_CLI_TEST_TIMEOUT Per CLI runner group curl timeout in seconds (default: 900).
USAGE
exit 0
;;
@ -42,5 +51,25 @@ if [[ ! -S "$socket_path" ]]; then
exit 1
fi
url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}"
exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
base_url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}&skip_local_service_pages=${skip_local_service_pages}"
if [[ "$action" == "list" ]]; then
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "$base_url"
exit 0
fi
if [[ "$action" == "run" ]]; then
groups=(demo http site doc-gate security task-lifetime starter tcp)
if [[ "$include_kill" == "1" ]]; then
groups+=(wasm-kill)
fi
for group in "${groups[@]}"; do
echo "== UCE CLI group: $group =="
curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "${base_url}&group=${group}"
done
scripts/test_dependency_invalidation.sh
scripts/test_cold_component_deadline.sh
scripts/test_nested_component_props.sh
scripts/test_password_hashing.sh
scripts/test_mysql_epoch_refresh.sh
scripts/test_log_timeliness.sh
fi

View File

@ -19,10 +19,23 @@ CONFIG_DEST="/etc/uce/settings.cfg"
action="${1:-setup}"
render_runtime_file() {
local source="$1"
local destination="$2"
local mode="$3"
local tmp
tmp="$(mktemp)"
trap 'rm -f "$tmp"' RETURN
sed "s#/Code/uce.openfu.com/uce#$REPO_ROOT#g" "$source" > "$tmp"
install -D -m "$mode" "$tmp" "$destination"
rm -f "$tmp"
trap - RETURN
}
install_unit() {
install -D -m 0644 "$UNIT_SOURCE" "$UNIT_DEST"
render_runtime_file "$UNIT_SOURCE" "$UNIT_DEST" 0644
if [[ ! -f "$CONFIG_DEST" ]]; then
install -D -m 0644 "$CONFIG_SOURCE" "$CONFIG_DEST"
render_runtime_file "$CONFIG_SOURCE" "$CONFIG_DEST" 0644
fi
systemctl daemon-reload
}

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="component-deadline-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
cleanup() {
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
printf '%s\n' \
'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce"
for i in $(seq 1 800); do
printf 'String cold_component_pad_%s() { return("%s"); }\n' "$i" "$i"
done >"$source_dir/child.uce"
printf '%s\n' \
'COMPONENT(Request& context) { <><strong>cold-component-deadline-ok</strong></> }' >>"$source_dir/child.uce"
rm -rf "$cache_dir"
output=$(scripts/uce-cli "/$test_name/parent.uce")
if [[ "$output" != *cold-component-deadline-ok* ]]; then
echo "cold component compilation consumed the guest epoch budget: $output" >&2
exit 1
fi
echo "cold component deadline passed"

View File

@ -0,0 +1,296 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="dependency-cache-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
mutation_file="/tmp/uce-dependency-mutation-$$"
post_body="/tmp/uce-dependency-post-body-$$"
post_headers="/tmp/uce-dependency-post-headers-$$"
lock_ready_file="/tmp/uce-dependency-lock-ready-$$"
nested_lock_ready_file="/tmp/uce-nested-lock-ready-$$"
race_output="/tmp/uce-source-race-output-$$"
race_error="/tmp/uce-source-race-error-$$"
race_status="/tmp/uce-source-race-status-$$"
http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}"
cleanup() {
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
rm -f "$mutation_file" "$post_body" "$post_headers" "$lock_ready_file" "$nested_lock_ready_file" "$race_output" "$race_error" "$race_status"
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
printf '%s\n' \
'#ifndef UCE_DEPENDENCY_CACHE_CHILD' \
'#define UCE_DEPENDENCY_CACHE_CHILD' \
'String dependency_cache_marker() { return("dependency-marker-a"); }' \
'#endif' >"$source_dir/child.uce"
printf '%s\n' \
'#load "child.uce"' \
'CLI(Request& context) { print(dependency_cache_marker(), ":", request_perf()["worker_pid"].to_string()); }' \
"RENDER(Request& context) { if(context.params[\"REQUEST_METHOD\"] == \"POST\") file_put_contents(\"$mutation_file\", dependency_cache_marker()); print(dependency_cache_marker(), \":\", request_perf()[\"worker_pid\"].to_string()); }" >"$source_dir/parent.uce"
assert_marker() {
local path="$1"
local expected="$2"
local output
output=$(scripts/uce-cli "/$test_name/$path.uce")
if [[ "$output" != *"$expected"* ]]; then
echo "$path returned stale module; expected $expected: $output" >&2
exit 1
fi
}
http_marker() {
curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce"
}
assert_marker parent dependency-marker-a
if [[ "$(http_marker)" != *"dependency-marker-a"* ]]; then
echo "HTTP warm-up did not return dependency-marker-a" >&2
exit 1
fi
printf '%s\n' 'CLI(Request& context) { print("readable-source-marker"); }' >"$source_dir/unreadable.uce"
chmod 000 "$source_dir/unreadable.uce"
if unreadable_output=$(scripts/uce-cli --get "/$test_name/unreadable.uce" __uce_expected_source_read_failure=1 2>&1); then
echo "unreadable source unexpectedly compiled: $unreadable_output" >&2
exit 1
fi
if [[ "$unreadable_output" != *"source file is not readable"* ]]; then
echo "unreadable source did not report its actual compile error: $unreadable_output" >&2
exit 1
fi
if [[ -e "$cache_dir/unreadable.uce.wasm" ]]; then
echo "unreadable source published a wasm artifact" >&2
exit 1
fi
chmod 644 "$source_dir/unreadable.uce"
assert_marker unreadable readable-source-marker
sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
started_at=$(date +%s%N)
http_during_rebuild=$(http_marker)
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
if [[ "$http_during_rebuild" != *"dependency-marker-a"* && "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
echo "HTTP rebuild request returned neither complete artifact: $http_during_rebuild" >&2
exit 1
fi
if (( elapsed_ms >= 2000 )); then
echo "HTTP request spent ${elapsed_ms}ms rebuilding a stale artifact" >&2
exit 1
fi
deadline=$((SECONDS + 15))
while [[ "$http_during_rebuild" != *"dependency-marker-b"* && $SECONDS -lt $deadline ]]; do
sleep 0.2
http_during_rebuild=$(http_marker)
done
if [[ "$http_during_rebuild" != *"dependency-marker-b"* ]]; then
echo "requested stale HTTP unit did not receive a demand-priority rebuild" >&2
exit 1
fi
assert_marker parent dependency-marker-b
# A proactive rebuild owns these same per-unit locks. While it publishes fresh
# artifacts, requests must use the last complete artifacts instead of waiting
# across the transitive graph. Atomic publication keeps those artifacts safe.
parent_wasm="$cache_dir/parent.uce.wasm"
child_wasm="$cache_dir/child.uce.wasm"
(
exec 8>"$parent_wasm.lock"
exec 9>"$child_wasm.lock"
flock 8
flock 9
: >"$lock_ready_file"
sleep 3
) &
rebuild_lock_pid=$!
deadline=$((SECONDS + 5))
while [[ ! -e "$lock_ready_file" && $SECONDS -lt $deadline ]]; do
if ! kill -0 "$rebuild_lock_pid" 2>/dev/null; then
echo "test rebuild lock process exited before acquiring locks" >&2
exit 1
fi
sleep 0.05
done
if [[ ! -e "$lock_ready_file" ]]; then
echo "test rebuild lock process did not acquire locks before deadline" >&2
exit 1
fi
sed -i 's/dependency-marker-b/dependency-marker-d/' "$source_dir/child.uce"
started_at=$(date +%s%N)
http_during_lock=$(http_marker)
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
if [[ "$http_during_lock" != *"dependency-marker-b"* ]]; then
echo "HTTP request did not serve the last complete artifact during rebuild: $http_during_lock" >&2
exit 1
fi
if (( elapsed_ms >= 2000 )); then
echo "HTTP request waited ${elapsed_ms}ms for an active transitive rebuild" >&2
exit 1
fi
rm -f "$mutation_file"
started_at=$(date +%s%N)
post_status=$(curl -sS -o "$post_body" -D "$post_headers" -w '%{http_code}' -X POST -H "Host: $http_host" "http://127.0.0.1/$test_name/parent.uce")
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
if [[ "$post_status" != "503" ]]; then
echo "stale POST executed an application artifact instead of returning 503: status=$post_status body=$(cat "$post_body")" >&2
exit 1
fi
if ! grep -qi '^Retry-After: 1' "$post_headers"; then
echo "stale POST did not return a Retry-After header" >&2
exit 1
fi
if [[ -e "$mutation_file" ]]; then
echo "stale POST executed the old mutation handler: $(cat "$mutation_file")" >&2
exit 1
fi
if (( elapsed_ms >= 2000 )); then
echo "stale POST waited ${elapsed_ms}ms instead of failing closed promptly" >&2
exit 1
fi
started_at=$(date +%s%N)
assert_marker parent dependency-marker-d
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
if (( elapsed_ms < 2000 )); then
echo "CLI request returned before the locked rebuild published the current artifact (${elapsed_ms}ms)" >&2
exit 1
fi
wait "$rebuild_lock_pid"
# CLI freshness applies to every nested component, not only the request entry
# unit. Hold the changed child's compile lock as a proactive rebuild would: the
# parent CLI request must wait and render the new child, never its stale wasm.
printf '%s\n' 'COMPONENT(Request& context) { print("nested-component-marker-a"); }' >"$source_dir/nested-child.uce"
printf '%s\n' \
'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce"
assert_marker nested-parent nested-component-marker-a
nested_child_wasm="$cache_dir/nested-child.uce.wasm"
(
exec 8>"$nested_child_wasm.lock"
flock 8
: >"$nested_lock_ready_file"
sleep 3
) &
nested_lock_pid=$!
deadline=$((SECONDS + 5))
while [[ ! -e "$nested_lock_ready_file" && $SECONDS -lt $deadline ]]; do
if ! kill -0 "$nested_lock_pid" 2>/dev/null; then
echo "nested component lock exited before acquisition" >&2
exit 1
fi
sleep 0.05
done
if [[ ! -e "$nested_lock_ready_file" ]]; then
echo "nested component lock was not acquired before deadline" >&2
exit 1
fi
sed -i 's/nested-component-marker-a/nested-component-marker-b/' "$source_dir/nested-child.uce"
started_at=$(date +%s%N)
nested_output=$(scripts/uce-cli "/$test_name/nested-parent.uce")
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
wait "$nested_lock_pid"
if [[ "$nested_output" != *"nested-component-marker-b"* ]]; then
echo "CLI nested component returned stale wasm: $nested_output" >&2
exit 1
fi
if (( elapsed_ms < 2000 )); then
echo "CLI nested component did not wait for the active rebuild (${elapsed_ms}ms)" >&2
exit 1
fi
# Warm every configured worker, then replace the artifact while retaining its
# whole-second mtime. The worker cache must notice the nanosecond/ctime change.
for _ in {1..16}; do assert_marker parent dependency-marker-d; done
sed 's/dependency-marker-d/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce"
sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce"
assert_marker alternate dependency-marker-c
alternate_wasm="$cache_dir/alternate.uce.wasm"
parent_mtime=$(stat -c %Y "$parent_wasm")
cp "$alternate_wasm" "$parent_wasm"
touch -d "@$parent_mtime" "$parent_wasm"
rm -f "$cache_dir/parent.uce.cwasm"
for _ in {1..16}; do assert_marker parent dependency-marker-c; done
race_unit="$source_dir/source-race.uce"
race_cpp="$cache_dir/source-race.uce.cpp"
{
printf '%s\n' 'String source_race_marker() { return("source-race-marker-a"); }'
for i in $(seq 1 3500); do
printf 'int source_race_padding_%s() { return(%s); }\n' "$i" "$i"
done
printf '%s\n' 'CLI(Request& context) { print(source_race_marker()); }'
} >"$race_unit"
rm -f "$cache_dir/source-race.uce.cpp" "$cache_dir/source-race.uce.wasm" "$cache_dir/source-race.uce.cwasm" "$cache_dir/source-race.uce.meta.txt"
(
set +e
scripts/uce-cli "/$test_name/source-race.uce" >"$race_output" 2>"$race_error"
echo "$?" >"$race_status"
) &
race_pid=$!
deadline=$((SECONDS + 10))
while [[ ! -e "$race_cpp" && $SECONDS -lt $deadline ]]; do
if ! kill -0 "$race_pid" 2>/dev/null; then
echo "source-race compile finished before publishing generated C++" >&2
cat "$race_output" "$race_error" >&2 || true
exit 1
fi
sleep 0.02
done
if [[ ! -e "$race_cpp" ]]; then
echo "source-race compile did not publish generated C++ before deadline" >&2
exit 1
fi
if ! grep -q 'source-race-marker-a' "$race_cpp"; then
echo "source-race generated C++ did not contain the initial marker" >&2
exit 1
fi
if ! kill -0 "$race_pid" 2>/dev/null; then
echo "source-race compile finished before source mutation hook" >&2
cat "$race_output" "$race_error" >&2 || true
exit 1
fi
sed -i 's/source-race-marker-a/source-race-marker-b/' "$race_unit"
wait "$race_pid"
if [[ "$(cat "$race_status" 2>/dev/null || echo 1)" != "0" ]]; then
echo "source-race CLI request failed" >&2
cat "$race_output" "$race_error" >&2 || true
exit 1
fi
if [[ "$(cat "$race_output")" != *"source-race-marker-b"* ]]; then
echo "source changed during compile but UCE served stale wasm: $(cat "$race_output")" >&2
exit 1
fi
worker_count=$(awk -F= '/^[[:space:]]*WORKER_COUNT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg 2>/dev/null || true)
worker_count="${worker_count:-4}"
worker_pids=""
for _ in {1..48}; do
output=$(scripts/uce-cli "/$test_name/parent.uce")
worker_pids+="${output##*:}"$'\n'
done
unique_workers=$(printf '%s' "$worker_pids" | sed '/^$/d' | sort -u | wc -l)
if (( unique_workers > worker_count )); then
echo "worker pool recycled during 48 requests: $unique_workers PIDs for $worker_count workers" >&2
exit 1
fi
echo "dependency invalidation passed"

49
scripts/test_log_timeliness.sh Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="log-timeliness-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
cleanup() {
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
printf '%s\n' 'CLI(Request& context) { deliberate_log_timeliness_compile_failure }' >"$source_dir/probe.uce"
rm -rf "$cache_dir"
started_at=$(date '+%Y-%m-%d %H:%M:%S')
set +e
scripts/uce-cli --get "/$test_name/probe.uce" __uce_expected_compile_failure=1 >/dev/null 2>&1
set -e
for _ in $(seq 1 20); do
journal_output=$(journalctl -u uce --since "$started_at" --no-pager)
if [[ "$journal_output" == *"UCE expected compile error"* && "$journal_output" == *"$test_name/probe.uce"* ]]; then
echo "log timeliness passed"
exit 0
fi
sleep 0.1
done
echo "compile diagnostic was not journaled promptly: $test_name/probe.uce" >&2
exit 1

View File

@ -0,0 +1,70 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="mysql-epoch-refresh-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r "$settings_file" ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file")
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
ticks=$(awk -F= '/^[[:space:]]*WASM_EPOCH_DEADLINE_TICKS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
period_ms=$(awk -F= '/^[[:space:]]*WASM_EPOCH_PERIOD_MS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true)
ticks="${ticks:-200}"
period_ms="${period_ms:-50}"
segment_seconds=$(awk -v ticks="$ticks" -v period="$period_ms" 'BEGIN { printf "%.6f", ticks * period * 0.00055 }')
cache_dir=""
test_user="uce_epoch_$$"
test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32)
test_user_created=false
cleanup() {
if [[ "$test_user_created" == true ]]; then
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true
fi
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'"
test_user_created=true
printf '%s\n' \
'void mysql_epoch_burn(f64 seconds) {' \
' f64 deadline = time_precise() + seconds;' \
' u64 spins = 0; while(time_precise() < deadline) spins++;' \
'}' \
'CLI(Request& context) {' \
" MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \
' if(db == 0 || mysql_error(db) != "") { print("mysql-connect-failed:", db == 0 ? "null" : mysql_error(db)); return; }' \
" mysql_epoch_burn($segment_seconds);" \
' mysql_query(db, "SELECT 1 AS value");' \
" mysql_epoch_burn($segment_seconds);" \
' mysql_query(db, "SELECT 2 AS value");' \
' if(mysql_error(db) == "") print("mysql-epoch-refresh-ok"); else print("mysql-query-failed:", mysql_error(db));' \
' mysql_disconnect(db);' \
'}' >"$source_dir/test.uce"
output=$(scripts/uce-cli "/$test_name/test.uce" 2>&1) || {
echo "MySQL hostcall did not refresh the guest epoch deadline: $output" >&2
exit 1
}
if [[ "$output" != "mysql-epoch-refresh-ok" ]]; then
echo "MySQL epoch refresh failed: $output" >&2
exit 1
fi
echo "MySQL epoch refresh passed with ${segment_seconds}s guest segments"

View File

@ -0,0 +1,65 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="nested-component-props-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
threshold_ms="${UCE_NESTED_COMPONENT_PROPS_MAX_MS:-1500}"
cleanup() {
rm -rf "$source_dir"
}
trap cleanup EXIT
mkdir -p "$source_dir"
printf '%s\n' \
'CLI(Request& context) {' \
' context.props["sentinel"] = "caller";' \
' String payload; for(u64 i = 0; i < 4096; ++i) payload += "x";' \
' DValue props; for(u64 i = 0; i < 300; ++i) { DValue item; item = payload; props["items"].push(item); }' \
' String output = component("outer", props, context);' \
' if(context.props["sentinel"].to_string() != "caller") { print("caller props not restored"); return; }' \
' print(output);' \
'}' >"$source_dir/parent.uce"
printf '%s\n' \
'COMPONENT(Request& context) {' \
' if(context.props["items"]._map.size() != 300) { print("outer props missing"); return; }' \
' for(u64 i = 0; i < 300; ++i) {' \
' DValue props; props["index"] = std::to_string(i); component("leaf", props, context);' \
' if(context.props["items"]._map.size() != 300) { print("outer props not restored"); return; }' \
' }' \
' print("nested-component-props-ok");' \
'}' >"$source_dir/outer.uce"
printf '%s\n' \
'COMPONENT(Request& context) {' \
' if(context.props["index"].to_string() == "") print("leaf props missing");' \
'}' >"$source_dir/leaf.uce"
warm_output=$(scripts/uce-cli "/$test_name/parent.uce")
if [[ "$warm_output" != "nested-component-props-ok" ]]; then
echo "nested component props warmup failed: $warm_output" >&2
exit 1
fi
start_ns=$(date +%s%N)
output=$(scripts/uce-cli "/$test_name/parent.uce")
elapsed_ms=$(( ($(date +%s%N) - start_ns) / 1000000 ))
if [[ "$output" != "nested-component-props-ok" ]]; then
echo "nested component props failed: $output" >&2
exit 1
fi
if (( elapsed_ms > threshold_ms )); then
echo "nested component props took ${elapsed_ms}ms (limit ${threshold_ms}ms)" >&2
exit 1
fi
echo "nested component props passed in ${elapsed_ms}ms"

View File

@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="password-hash-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then
configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
if [[ -n "${configured_site_directory:-}" ]]; then
site_directory="$configured_site_directory"
fi
fi
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
cleanup() {
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
printf '%s\n' \
'CLI(Request& context) {' \
' String encoded = password_hash("correct horse battery staple");' \
' String second = password_hash("correct horse battery staple");' \
' String weaker = "$uce$scrypt$16384$8$1$00112233445566778899aabbccddeeff$29fdfb3d991961e926a19c1136a07e252afa5fdb8d3a0fb74cdcfa5016956f34";' \
' String excessive = "$uce$scrypt$131072$8$1$00112233445566778899aabbccddeeff$0000000000000000000000000000000000000000000000000000000000000000";' \
' print(encoded, "\n", encoded != second ? "randomized" : "reused", "\n", password_verify("correct horse battery staple", encoded) ? "valid" : "invalid", "\n", password_verify("wrong password", encoded) ? "wrong-valid" : "wrong-rejected", "\n", password_needs_rehash(encoded) ? "rehash" : "current", "\n", password_verify("legacy password", weaker) && password_needs_rehash(weaker) ? "legacy-valid-rehash" : "legacy-failed", "\n", !password_verify("password", excessive) && password_needs_rehash(excessive) ? "excessive-rejected" : "excessive-accepted", "\n", !password_verify("password", "$uce$scrypt$65536$8$1$zz$00") && password_needs_rehash("malformed") ? "malformed-rejected" : "malformed-valid");' \
'}' >"$source_dir/test.uce"
output=$(scripts/uce-cli "/$test_name/test.uce")
if [[ "$output" != *'$uce$scrypt$65536$8$1$'* || "$output" != *$'\nrandomized\nvalid\nwrong-rejected\ncurrent\nlegacy-valid-rehash\nexcessive-rejected\nmalformed-rejected'* ]]; then
echo "native password hashing failed: $output" >&2
exit 1
fi
echo "password hashing passed"

View File

@ -6,9 +6,9 @@ RENDER(Request& context)
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, "/", " / "))); });
StringList unique_routes = routes.unique();
StringList sorted_routes = unique_routes.sort();
StringList labels = sorted_routes.map([](String route) { return(to_upper(replace(route, "/", " / "))); });
DValue cards;
DValue card;
@ -16,9 +16,8 @@ RENDER(Request& context)
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);
DValue app_cards = dv_filter(cards, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue titles = dv_map(app_cards, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
DValue grouped = dv_group_by(cards, [](DValue item, String key) { return(item["section"].to_string()); });
DValue app_cards = cards.filter([](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue titles = app_cards.map([](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); });
<><html>
<head>
@ -31,7 +30,7 @@ RENDER(Request& context)
<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>DValue app card titles</h3><pre><?= json_encode(titles) ?></pre></div>
<div class="system-info"><h3>Grouped cards</h3><pre><?= json_encode(grouped) ?></pre></div>
<div class="system-info"><h3>All card values</h3><pre><?= json_encode(cards.values()) ?></pre></div>
<details>
<summary>Request Parameters</summary>
<pre><?= var_dump(p) ?></pre>

View File

@ -91,8 +91,16 @@ RENDER(Request& context)
print("Accept us: ", (f64)perf["accept_us"].to_f64(), "\n");
print("Running us: ", (f64)perf["running_us"].to_f64(), "\n");
print("Total us: ", (f64)perf["total_us"].to_f64(), "\n");
if(perf["workspace_birth_us"].type != 'S')
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
print("Dispatch us: ", (u64)perf["dispatch_us"].to_u64(), "\n");
print("Workspace setup us: ", (u64)perf["workspace_setup_us"].to_u64(), "\n");
print("Workspace birth us: ", (u64)perf["workspace_birth_us"].to_u64(), "\n");
print("Context apply us: ", (u64)perf["context_apply_us"].to_u64(), "\n");
print("Guest us: ", (f64)perf["guest_us"].to_f64(), "\n");
print("Hostcalls: ", (u64)perf["hostcall_count"].to_u64(), " / ", (u64)perf["hostcall_us"].to_u64(), " us\n");
print("MySQL hostcalls: ", (u64)perf["mysql_hostcall_count"].to_u64(), " / ", (u64)perf["mysql_hostcall_us"].to_u64(), " us\n");
print("MySQL operations: ", json_encode(perf["mysql_operations"]), " / dropped ", (u64)perf["mysql_operations_dropped"].to_u64(), "\n");
print("Memcache hostcalls: ", (u64)perf["memcache_hostcall_count"].to_u64(), " / ", (u64)perf["memcache_hostcall_us"].to_u64(), " us\n");
print("Component resolves: ", (u64)perf["component_resolve_count"].to_u64(), " / ", (u64)perf["component_resolve_us"].to_u64(), " us\n");
print("Output buffer size: ", context.ob->str().length(), "\n");
?></pre>
</div>

View File

@ -6,3 +6,5 @@ memcache_delete
memcache_get
memcache_get_multiple
memcache_set
memcache_escape_key
memcache_escape_keys

View File

@ -5,4 +5,5 @@ mysql_disconnect
mysql_error
mysql_escape
mysql_insert_id
mysql_affected_rows
mysql_query

View File

@ -8,3 +8,12 @@ gen_noise64
gen_float
gen_int
gen_sha1
sha256
sha256_hex
hmac_sha256
hmac_sha256_hex
random_bytes
crypto_equal
password_hash
password_verify
password_needs_rehash

View File

@ -1,8 +1,9 @@
Runtime
backtrace_frames_string
capture_backtrace_string
backtrace_get_frames
backtrace_capture
error_pages
signal_name
unit_info
units_list
unit_compile
3_Documentation format

View File

@ -1,5 +1,7 @@
Socket Functions
http_request
http_request_async
socket_close
socket_connect
socket_read

View File

@ -9,12 +9,7 @@ first
join
json_consume_space
json_encode
list_every
list_find
map
list_some
list_sort
list_unique
nibble
print
strpos
@ -35,3 +30,9 @@ regex_split
to_lower
to_upper
trim
float_val
int_val
html_escape
safe_name
var_dump
json_decode

View File

@ -4,7 +4,6 @@ basename
dirname
expand_path
file_append
file_append_contents
file_exists
file_get_contents
file_mtime
@ -16,6 +15,11 @@ path_join
cwd_set
shell_escape
shell_exec
shell_spawn
job_status
job_result
job_await
job_cancel
file_unlink
zip_create
zip_list
@ -25,3 +29,26 @@ gz_compress
gz_uncompress
server_start_http
server_stop
file_open
file_read
file_pread
file_write
file_pwrite
file_seek
file_tell
file_close
file_stat
dir_list
file_rename
file_copy
file_truncate
dir_remove
file_temp
file_chmod
file_symlink
file_fsync
path_is_within
path_real
process_start_directory
request_perf
runtime_safe_key

View File

@ -3,26 +3,38 @@ Types
0_Request
array_merge
0_DValue
dv_filter
dv_group_by
dv_keys
dv_map
dv_omit
dv_pick
dv_values
each
get_by_path
has
is_array
is_list
set_status
String
StringList
StringMap
2_DValue_filter
2_DValue_keys
2_DValue_map
2_DValue_values
2_DValue_each
2_DValue_get_by_path
2_DValue_has
2_DValue_is_array
2_DValue_is_list
2_Request_set_status
0_String
0_StringList
2_StringList_filter
2_StringList_map
2_StringList_keys
2_StringList_each
2_StringList_unique
2_StringList_sort
2_StringList_some
2_StringList_every
2_StringList_find
0_StringMap
2_DValue_to_bool
2_DValue_to_f64
2_DValue_to_json
2_DValue_to_s64
2_DValue_to_string
2_DValue_to_stringmap
2_DValue_to_u64
to_bool
to_f64
to_json
to_s64
to_string
to_stringmap
to_u64
ucb_encode
ucb_decode

View File

@ -15,3 +15,4 @@ parse_query
parse_uri
uri_decode
uri_encode
request_route_from_raw_path

View File

@ -0,0 +1,16 @@
// Small target unit used by the unit_*/component documentation examples.
RENDER(Request& context)
{
print("hello from the sample unit");
}
COMPONENT(Request& context)
{
print("hello from the sample component");
}
EXPORT DValue* doc_greet(DValue* call_param)
{
print("doc_greet() was called");
return(0);
}

View File

@ -45,6 +45,64 @@ void render_doc_params(StringList param_lines)
?></div></>
}
String doc_example_path(Request& context, String page, u64 idx)
{
String bin_dir = context.params["UCE_BIN_DIRECTORY"];
if(bin_dir == "")
return("");
String dir = path_join(bin_dir, "doc-examples");
mkdir(dir);
return(path_join(dir, page + "_" + std::to_string(idx + 1) + ".uce"));
}
String doc_example_unit_source(String body)
{
return("RENDER(Request& context)\n{\n" + body + "\n}\n");
}
bool doc_write_if_changed(String path, String content)
{
if(file_exists(path) && file_get_contents(path) == content)
return(true);
return(file_put_contents(path, content));
}
String doc_render_example_output(Request& context, String page, u64 idx, String body)
{
String path = doc_example_path(context, page, idx);
if(path == "")
return("DOC EXAMPLE ERROR: UCE_BIN_DIRECTORY is not available");
String unit_source = doc_example_unit_source(body);
if(!doc_write_if_changed(path, unit_source))
return("DOC EXAMPLE ERROR: could not write " + path);
if(!unit_compile(path))
return("DOC EXAMPLE ERROR: unit_compile failed for " + path);
ob_start();
unit_render(path);
String out = ob_get_close();
if(out == "")
return("DOC EXAMPLE ERROR: example produced no output");
return(out);
}
void render_doc_examples(Request& context, String page, StringList example_blocks)
{
for(u64 idx = 0; idx < example_blocks.size(); idx++)
{
String body = example_blocks[idx];
String output = doc_render_example_output(context, page, idx, body);
?><div class="doc-section example">
<h3>Example</h3>
<pre class="example-source"><?= body ?></pre>
<div class="example-output">
<div class="example-output-label">Output</div>
<pre><?= output ?></pre>
</div>
</div><?
}
}
void render_see_section(String name)
{
StringList lines = split(file_get_contents("areas/" + name + ".txt"), "\n");
@ -225,6 +283,8 @@ RENDER(Request& context)
?><div class="doc-section content"><?: markdown_to_html(doc_page.content) ?></div><?
}
render_doc_examples(context, page, doc_page.example_blocks);
?></article><?
if(doc_page.see_lines.size() > 0)

View File

@ -5,6 +5,7 @@ struct DocPage {
String content;
StringList sig_lines;
StringList param_lines;
StringList example_blocks;
StringList see_lines;
};
@ -13,12 +14,25 @@ enum class DocPageKind
function,
struct_page,
directive,
method,
info
};
String doc_method_label(String page)
{
String label = page;
nibble(label, "_");
String class_name = nibble(label, "_");
if(label == "")
return(class_name);
return(class_name + "::" + label);
}
String doc_default_title(String page)
{
String page_title = page;
if(page.substr(0, 2) == "2_")
return(doc_method_label(page));
if(page_title.length() > 1 && page_title[1] == '_')
nibble(page_title, "_");
return(page_title);
@ -60,6 +74,8 @@ DocPageKind doc_page_kind(String page)
return(DocPageKind::struct_page);
if(page.substr(0, 2) == "1_")
return(DocPageKind::directive);
if(page.substr(0, 2) == "2_")
return(DocPageKind::method);
if(page.substr(0, 2) == "3_")
return(DocPageKind::info);
return(DocPageKind::function);
@ -71,6 +87,8 @@ String doc_page_kind_badge(DocPageKind kind)
return("struct");
if(kind == DocPageKind::directive)
return("directive");
if(kind == DocPageKind::method)
return("method");
if(kind == DocPageKind::info)
return("info");
return("");
@ -80,35 +98,76 @@ String doc_index_label(String page)
{
String label = page;
auto kind = doc_page_kind(page);
if(kind == DocPageKind::method)
return(doc_method_label(page));
if(kind == DocPageKind::struct_page || kind == DocPageKind::directive || kind == DocPageKind::info)
nibble(label, "_");
return(label);
}
void doc_flush_section(DocPage& result, String page, String section, StringList& section_lines, StringList& content_lines)
{
if(section == "")
return;
if(section == "title")
{
String title = trim(join(section_lines, "\n"));
if(title != page)
result.title = title;
}
else if(section == "sig")
{
for(String line : section_lines)
result.sig_lines.push_back(line);
}
else if(section == "params")
{
for(String line : section_lines)
result.param_lines.push_back(line);
}
else if(section == "see")
{
for(String line : section_lines)
{
line = trim(line);
if(line != "")
result.see_lines.push_back(line);
}
}
else if(section == "example")
{
String example = join(section_lines, "\n");
if(trim(example) != "")
result.example_blocks.push_back(example);
}
else
{
for(String line : section_lines)
content_lines.push_back(line);
}
}
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 current_lines;
StringList content_lines;
for(auto line : lines)
{
if(!content_mode && line != "" && line.substr(0, 1) == ":")
if(line != "" && line.substr(0, 1) == ":")
{
doc_flush_section(result, page, current_section, current_lines, content_lines);
current_lines.clear();
String section = trim(line.substr(1));
if(section == "title" || section == "sig" || section == "params" || section == "see")
if(section == "title" || section == "sig" || section == "params" || section == "content" || section == "example" || 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);
@ -122,31 +181,10 @@ DocPage load_doc_page(String page)
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);
}
current_lines.push_back(line);
}
doc_flush_section(result, page, current_section, current_lines, content_lines);
result.content = join(content_lines, "\n");
result.title = trim(result.title);
return(result);

View File

@ -6,7 +6,7 @@ DValue
:see
>types
get_by_path
2_DValue_get_by_path
json_decode
:content
@ -47,12 +47,8 @@ You will encounter `DValue` throughout the runtime, especially in:
- `.to_bool(default)` performs best-effort boolean conversion.
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
All read accessors are `const` and never modify the tree; they work directly on `const DValue&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`2_DValue_to_string`, `2_DValue_to_s64`, `2_DValue_to_u64`, `2_DValue_to_f64`, `2_DValue_to_bool`) for the exact rules:
```cpp
String title = context.props["title"].to_string("Untitled");
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
```
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
@ -100,7 +96,7 @@ Useful inspection helpers include:
## each()
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
`each(std::function<void (const DValue& t, String key)> f)` iterates over the current tree value (see `2_DValue_each` for details).
For map-shaped `DValue` values, the callback runs once per child entry and receives:
@ -112,31 +108,12 @@ For non-map values, `each()` still invokes the callback once:
- `t` is the current value
- `key` is an empty string
```cpp
context.connection["items"].each([&](const DValue& item, String key) {
print(key, ": ", item.to_string(), "\n");
});
```
## Examples
```cpp
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.props["dark_mode"].to_bool();
if(DValue* user = payload.key("user")) {
print(user->to_json());
}
payload.get_or_create("headers")->get_or_create("Content-Type")->set("text/plain");
StringMap headers = payload["headers"].to_stringmap();
```
## Related Concepts
- PHP: nested associative arrays, `stdClass`, decoded JSON trees, and helper accessors for deep array paths
- JavaScript / Node.js: plain objects, arrays, `Map`, and JSON-shaped data passed between handlers
:example
DValue user;
user["name"] = "Ada";
user["active"] = true;
print(user["name"].to_string(), "\n");

View File

@ -11,7 +11,7 @@ request_script_url
request_base_url
request_query_path
request_query_route
set_status
2_Request_set_status
component
component_render
unit_render
@ -25,19 +25,12 @@ ws_connection_id
ws_connections
ws_send
0_DValue
StringMap
0_StringMap
UploadedFile
:content
`Request& context` is the request-local state object passed into every UCE handler:
```cpp
RENDER(Request& context) { ... }
COMPONENT(Request& context) { ... }
WS(Request& context) { ... }
CLI(Request& context) { ... }
SERVE_HTTP(Request* req) { ... }
```
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.
@ -88,9 +81,6 @@ 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`.
@ -100,10 +90,6 @@ 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`
@ -119,9 +105,6 @@ Raw request body. For WebSocket handlers, this is the current message payload.
Use this for JSON APIs:
```cpp
DValue body = json_decode(context.in);
```
### `context.uploaded_files`
@ -143,10 +126,6 @@ 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.
@ -171,11 +150,6 @@ General request-local scratch/configuration tree. It is shared by the page, comp
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`).
@ -185,17 +159,11 @@ Type: `DValue`
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`
@ -203,11 +171,6 @@ Type: `DValue`
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
DValue props;
props["title"] = "Dashboard";
print(component("components/card", props, context));
```
### `context.connection`
@ -215,9 +178,6 @@ Type: `DValue`
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.
@ -235,10 +195,6 @@ 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`
@ -250,11 +206,6 @@ Queued `Set-Cookie` header lines. Prefer `set_cookie()` instead of editing this
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:
@ -276,12 +227,6 @@ Internal output-buffer stack. Most code should use helpers instead of touching t
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`
@ -358,9 +303,6 @@ Use `context.connection` for per-socket structured state.
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`.
@ -381,57 +323,21 @@ Notable fields:
### Minimal page
```cpp
RENDER(Request& context)
{
<><h1>Hello <?= context.get["name"] ?></h1></>
}
```
### JSON endpoint
```cpp
RENDER(Request& context)
{
context.header["Content-Type"] = "application/json";
DValue 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
DValue 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()`, 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
:example
print(context.params["REQUEST_URI"] != "" ? "request available\n" : "request available\n");

View File

@ -1,5 +1,5 @@
:sig
String
0_String
:see
>types
@ -19,7 +19,6 @@ Because it is backed by `std::string`, it is binary-safe and may also contain ra
For UTF-8-aware splitting, use helpers such as `split_utf8()` instead of assuming one byte equals one character.
## Related Concepts
- PHP: native PHP strings with helpers such as `substr()`, `trim()`, `explode()`, and `implode()`
- JavaScript / Node.js: JavaScript `string` values with methods such as `slice()`, `trim()`, `split()`, and `join()`
:example
String name = "Ada";
print(to_upper(name), "\n");

View File

@ -0,0 +1,27 @@
:sig
struct StringList — ordered list of String values
:params
return value : use methods such as `filter()`, `map()`, `keys()`, `each()`, `unique()`, `sort()`, `some()`, `every()`, and `find()` to derive or inspect lists
:content
`StringList` is an ordered container of `String` values. It inherits normal `std::vector<String>` behavior and adds list helpers for filtering, mapping, indexes, iteration, deduplication, sorting, and predicate checks.
:example
StringList labels = split("a,b,a", ",").unique().map([](String item) { return(to_upper(item)); });
print(join(labels, ","), "\n");
:see
>types
0_String
split
join
2_StringList_filter
2_StringList_map
2_StringList_keys
2_StringList_each
2_StringList_unique
2_StringList_sort
2_StringList_some
2_StringList_every
2_StringList_find

View File

@ -1,5 +1,5 @@
:sig
StringMap
0_StringMap
:see
>types
@ -28,7 +28,7 @@ Because it uses `std::map`, `map["key"]` will create an empty entry when that ke
Helpers such as `parse_query()` and `encode_query()` convert between query strings and `StringMap` values.
## Related Concepts
- PHP: associative arrays keyed by string, especially request bags like `$_GET` and `$_SERVER`
- JavaScript / Node.js: plain objects, dictionaries, `Map`, and header or query objects in web frameworks
:example
StringMap params;
params["page"] = "docs";
print(params["page"], "\n");

View File

@ -5,6 +5,7 @@ CLI
CLI(Request& context)
:see
>ob
>1_RENDER
>1_COMPONENT
>1_WS
@ -23,17 +24,9 @@ The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_S
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.
@ -48,20 +41,8 @@ CLI responses default to `text/plain; charset=utf-8`, but the handler may set he
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`.
```cpp
CLI(Request& context)
{
DValue 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.
:example
print("CLI is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -37,19 +37,6 @@ When you call `component(":NAME", props, context)` or `component_render(":NAME",
## Example
```cpp
COMPONENT(Request& context)
{
<><section><?: component(":BODY", context.props, context) ?></section></>
}
COMPONENT:BODY(Request& context)
{
<><p><?= context.props["body"] ?></p></>
}
```
## Related Concepts
- PHP: view partials, reusable include files, small template helpers, and server-side component-like rendering patterns
- JavaScript / Node.js: reusable component functions, React or Vue components, and server-rendered partials
:example
print("COMPONENT is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -27,24 +27,6 @@ Because UCE usually loads units on demand during a request, `INIT()` still recei
## 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
:example
print("INIT is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -27,23 +27,6 @@ When a request first enters a given file through `RENDER(Request& context)`, `CO
## 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
:example
print("ONCE is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -40,7 +40,5 @@ In that case:
- `WS(Request& context)` handles later WebSocket messages
- `COMPONENT()` remains available only through the component helpers
## Related Concepts
- PHP: front controller entrypoints, template files, `include`, `require`, and route handlers that write the HTTP response
- JavaScript / Node.js: Express or Fastify route handlers, page controller functions, and SSR entrypoints that build a response
:example
print("RENDER is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -40,7 +40,5 @@ The current message metadata is available as:
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
- PHP: Ratchet `onMessage`, Workerman WebSocket handlers, or lower-level callbacks around accepted socket connections
- JavaScript / Node.js: browser `WebSocket` `message` handlers and Node `ws` server `connection` and `message` callbacks
:example
print("WS is a unit directive; place it at top level in a .uce unit.\n");

View File

@ -0,0 +1,28 @@
:title
DValue::operator=
:sig
void DValue::operator=(String v)
void DValue::operator=(f64 v)
void DValue::operator=(void* v)
void DValue::operator=(DValue v)
void DValue::operator=(StringMap v)
:params
v : assigned value
return value : none
:see
0_DValue
2_DValue_set
2_DValue_set_bool
:content
Assignment overloads forward to the matching `set(...)` overloads. They are the normal concise way to write string, number, pointer, `DValue`, and `StringMap` values. Boolean values should use `set_bool()` to avoid overload ambiguity.
:example
DValue row;
row["name"] = "Ada";
row["role"] = "engineer";
print(row["name"].to_string(), " / ", row["role"].to_string(), "\n");

View File

@ -0,0 +1,19 @@
:sig
void DValue::clear()
:params
return value : none
:see
0_DValue
2_DValue_set_array
:content
Clears the value into an empty map-shaped value and resets its array index. Unlike `set_array()`, this does not set list mode.
:example
DValue u;
u["a"] = "1"; u["b"] = "2";
u.clear();
print("after clear, has a: ", u.has("a") ? "yes" : "no", "\n");

View File

@ -0,0 +1,21 @@
:sig
DValue& DValue::deref()
const DValue& DValue::deref() const
:params
return value : referenced target when resolvable, otherwise this value
:see
0_DValue
2_DValue_is_reference
:content
Returns the value after following internal reference links. Most public accessors call this for you, so direct use is rare in unit code.
:example
DValue target;
target.set("value");
DValue ref;
ref.set_reference(&target);
print("through ref: ", ref.deref().to_string(), "\n");

View File

@ -0,0 +1,26 @@
:sig
void DValue::each(std::function<void (const DValue& item, String key)> f) const
:params
f : callback invoked for each child, or once for a scalar value
return value : none
:see
>types
0_DValue
2_DValue_map
2_DValue_filter
2_DValue_is_list
:content
Iterates a `DValue` without modifying it. References are dereferenced before iteration.
For map-shaped values, the callback receives each child and its key. List-shaped maps iterate in numeric index order (`0`, `1`, `2`, ...); ordinary maps iterate in `std::map` string-key order. Scalar values invoke the callback once with the current value and an empty key.
:example
DValue user;
user["name"] = "Ada";
user["role"] = "admin";
StringList keys;
user.each([&](const DValue& item, String key) { keys.push_back(key); });
print(join(keys.sort(), ","), "\n");

View File

@ -0,0 +1,26 @@
:sig
DValue DValue::filter(StringList keys) const
DValue DValue::filter(std::function<bool (const DValue& item, String key)> f) const
:params
keys : child keys to copy when using the key-list overload
f : predicate returning true for children to keep
return value : filtered copy
:see
>types
0_DValue
2_DValue_map
2_DValue_keys
:content
`filter(keys)` copies existing children with the named keys into a new map-shaped `DValue`.
`filter(f)` calls the predicate for each item from `each()`. Kept children from list-shaped input are pushed into a new list and re-indexed from zero; kept children from map-shaped input keep their original keys. Scalar input is passed to the predicate once and, if accepted, is pushed into a list.
:example
DValue user;
user["name"] = "Ada";
user["email"] = "ada@example.test";
DValue public_user = user.filter({"name"});
print(public_user["name"].to_string(), "\n");

View File

@ -0,0 +1,21 @@
:sig
DValue DValue::get_by_path(String path, String delim = "/") const
:params
path : delimited child path to traverse
delim : path separator string
return value : resolved child copy, or an empty DValue when traversal fails
:see
>types
0_DValue
2_DValue_has
2_DValue_to_string
:content
Traverses nested map/list children without creating missing nodes. Empty path segments are ignored. If any segment is missing or an intermediate value is not map-shaped, the method returns an empty `DValue`.
:example
DValue cfg;
cfg["site"]["title"] = "UCE";
print(cfg.get_by_path("site/title").to_string(), "\n");

View File

@ -0,0 +1,20 @@
:sig
DValue* DValue::get_or_create(String s)
:params
s : child key to create or return
return value : pointer to the child node
:see
0_DValue
2_DValue_key
2_DValue_has
:content
Ensures the value is map-shaped and returns the named child. If the key does not exist, an empty child is created. Creating a non-numeric key on a list-shaped value clears list mode.
:example
DValue config;
config.get_or_create("host")->set("localhost");
print(config.has("host") ? config["host"].to_string() : "missing", "\n");

View File

@ -0,0 +1,17 @@
:sig
String DValue::get_type_name() const
:params
return value : one of String, f64, bool, array, pointer, reference, or unknown
:see
0_DValue
:content
Returns a human-readable name for the dereferenced value's type tag.
:example
DValue s; s.set("text");
DValue m; m["k"] = "v";
print(s.get_type_name(), " / ", m.get_type_name(), "\n");

View File

@ -0,0 +1,20 @@
:sig
bool DValue::has(String s) const
:params
s : child key to test
return value : true when the dereferenced value is map-shaped and contains the key
:see
>types
0_DValue
2_DValue_key
2_DValue_get_by_path
:content
Checks for a child key without creating it. Returns false for scalars and missing keys.
:example
DValue u;
u["name"] = "Ada";
print(u.has("name") ? "has name" : "no name", " / ", u.has("age") ? "has age" : "no age", "\n");

View File

@ -0,0 +1,18 @@
:sig
bool DValue::is_array() const
:params
return value : true when the dereferenced value is map-shaped
:see
>types
0_DValue
2_DValue_is_list
:content
Returns true when the value's current type is `M`, the map/list container type. It does not require contiguous numeric keys; use `is_list()` for that.
:example
DValue user;
user["name"] = "Ada";
print(user.is_array() ? "array\n" : "scalar\n");

View File

@ -0,0 +1,20 @@
:sig
bool DValue::is_list() const
:params
return value : true when the value is an array with keys 0..n-1
:see
>types
0_DValue
2_DValue_set_array
2_DValue_push
:content
Returns true for map-shaped values that represent a list. An empty map is a list only if it was explicitly put in list mode with `set_array()` or by list operations. Non-empty lists must have canonical numeric string keys from `0` through `size - 1`.
:example
DValue list;
DValue item; item = "Ada";
list.push(item);
print(list.is_list() ? "list\n" : "map\n");

View File

@ -0,0 +1,20 @@
:sig
bool DValue::is_reference() const
:params
return value : true when this node itself is a reference node
:see
0_DValue
2_DValue_reference_target
2_DValue_deref
:content
Reports whether the current node's direct type tag is the internal reference tag. Most read methods dereference automatically; this helper is mainly useful when handling reference-aware runtime state.
:example
DValue target; target.set("x");
DValue ref; ref.set_reference(&target);
DValue plain; plain.set("y");
print(ref.is_reference() ? "ref" : "plain", " / ", plain.is_reference() ? "ref" : "plain", "\n");

View File

@ -0,0 +1,23 @@
:sig
DValue* DValue::key(String s)
const DValue* DValue::key(String s) const
:params
s : child key to find
return value : pointer to existing child, or 0
:see
0_DValue
2_DValue_has
2_DValue_get_or_create
:content
Looks up one child without creating it. The non-const overload forwards through references; both overloads return `0` for scalars or missing keys.
:example
DValue user;
user["email"] = "ada@example.test";
const DValue* found = user.key("email");
const DValue* missing = user.key("phone");
print(found ? found->to_string() : "none", " / ", missing ? "present" : "absent", "\n");

View File

@ -0,0 +1,20 @@
:sig
StringList DValue::keys() const
:params
return value : child keys for map/list values; empty for scalars
:see
>types
0_DValue
2_DValue_each
0_StringList
:content
Returns the keys produced by `each()`. Scalar values have no child key, so the result is empty. List-shaped values return their numeric keys as strings.
:example
DValue user;
user["name"] = "Ada";
user["role"] = "admin";
print(join(user.keys().sort(), ","), "\n");

View File

@ -0,0 +1,24 @@
:sig
DValue DValue::map(std::function<DValue (const DValue& item, String key)> f) const
:params
f : mapper called for each iterated item
return value : mapped copy
:see
>types
0_DValue
2_DValue_each
2_DValue_filter
:content
Builds a new `DValue` by replacing each iterated child with the mapper's returned value. List-shaped input stays list-shaped and is re-indexed by `push()`. Map-shaped input preserves keys. Scalar input is mapped once and pushed into a list.
:example
DValue names;
DValue first; first = "ada";
DValue second; second = "grace";
names.push(first);
names.push(second);
DValue upper = names.map([](const DValue& item, String key) { DValue out; out = to_upper(item.to_string()); return(out); });
print(upper["0"].to_string(), ",", upper["1"].to_string(), "\n");

View File

@ -0,0 +1,24 @@
:title
DValue::operator[]
:sig
DValue& DValue::operator[](String s)
:params
s : child key to return or create
return value : reference to the child node
:see
0_DValue
2_DValue_get_or_create
2_DValue_key
2_DValue_has
:content
Returns a mutable reference to a child, creating the child when it does not already exist. If this value is an internal reference, the operation is forwarded to the target. This has the same creation behavior as `get_or_create()`, so use `has()`, `key()`, or `get_by_path()` for non-mutating reads.
:example
DValue m;
m["a"]["b"] = "nested";
print(m["a"]["b"].to_string(), "\n");

View File

@ -0,0 +1,21 @@
:sig
DValue DValue::pop()
:params
return value : removed child, or an empty DValue when no child exists
:see
0_DValue
2_DValue_push
:content
Ensures the value is map-shaped, removes the last entry according to `std::map` reverse key order, and returns it. For list-mode values, the next array index is reset to the new size.
:example
DValue list;
list.set_array();
DValue a; a.set("first"); list.push(a);
DValue b; b.set("last"); list.push(b);
DValue popped = list.pop();
print("popped ", popped.to_string(), ", remaining ", list.keys().size(), "\n");

View File

@ -0,0 +1,22 @@
:sig
void DValue::push(const DValue& child)
:params
child : value to append/copy
return value : none
:see
0_DValue
2_DValue_pop
2_DValue_set_array
:content
Appends a child under the next numeric key. Empty values become list-shaped. Existing contiguous lists continue at their size; non-list maps use the next unused numeric key and remain non-list.
:example
DValue list;
list.set_array();
DValue a; a.set("x"); list.push(a);
DValue b; b.set("y"); list.push(b);
print("count: ", list.keys().size(), "\n");

View File

@ -0,0 +1,22 @@
:sig
DValue* DValue::reference_target()
const DValue* DValue::reference_target() const
:params
return value : resolved target pointer, or 0 when this node is not a usable reference
:see
0_DValue
2_DValue_is_reference
2_DValue_deref
:content
If this node is an internal reference, follows reference links up to the runtime safety limit and returns the final non-reference target. Returns `0` for non-references, null/self references, or chains that still resolve to a reference.
:example
DValue target;
target.set("data");
DValue ref;
ref.set_reference(&target);
print(ref.reference_target() == &target ? "points to target" : "elsewhere", "\n");

View File

@ -0,0 +1,21 @@
:sig
void DValue::remove(String s)
:params
s : child key to erase
return value : none
:see
0_DValue
2_DValue_clear
2_DValue_has
:content
Ensures the value is map-shaped and erases the named child. Removing the last child resets the next array index to zero.
:example
DValue u;
u["a"] = "1"; u["b"] = "2";
u.remove("a");
print("has a: ", u.has("a") ? "yes" : "no", ", has b: ", u.has("b") ? "yes" : "no", "\n");

View File

@ -0,0 +1,27 @@
:sig
void DValue::set(String s)
void DValue::set(void* p)
void DValue::set(f64 f)
void DValue::set(DValue source)
void DValue::set(StringMap source)
:params
s : string value for the String overload
p : pointer value for the pointer overload
f : floating point value for the f64 overload
source : DValue or StringMap to copy
return value : none
:see
0_DValue
2_DValue_set_bool
2_DValue_set_array
:content
Assigns a new value, forwarding through references when possible. String, pointer, and f64 overloads set scalar nodes. `set(DValue)` copies the source's current type and payload. `set(StringMap)` creates a map-shaped value with each string entry as a child.
:example
DValue v;
v.set("hello");
print(v.to_string(), "\n");

View File

@ -0,0 +1,21 @@
:sig
void DValue::set_array()
:params
return value : none
:see
0_DValue
2_DValue_push
2_DValue_is_list
:content
Clears the value and makes it an empty list-shaped map. Subsequent `push()` calls append numeric keys starting at `0`.
:example
DValue list;
list.set_array();
DValue item; item.set("first");
list.push(item);
print("is_list=", list.is_list() ? "yes" : "no", " count=", list.keys().size(), "\n");

View File

@ -0,0 +1,19 @@
:sig
void DValue::set_bool(bool b)
:params
b : boolean value to store
return value : none
:see
0_DValue
2_DValue_to_bool
:content
Stores a boolean value, forwarding through references when possible.
:example
DValue flag;
flag.set_bool(true);
print(flag.to_bool() ? "true" : "false", "\n");

View File

@ -0,0 +1,21 @@
:sig
void DValue::set_reference(DValue* target)
:params
target : target node pointer
return value : none
:see
0_DValue
2_DValue_reference_target
:content
Stores an internal reference to another `DValue`. Most unit code should not need to create references directly; normal reads and writes generally follow existing references automatically.
:example
DValue target;
target.set("original");
DValue ref;
ref.set_reference(&target);
print(ref.deref().to_string(), "\n");

View File

@ -0,0 +1,23 @@
:sig
void DValue::set_type(char t)
:params
t : internal type tag such as S, F, B, M, P, or R
return value : none
:see
0_DValue
2_DValue_set
2_DValue_set_array
:content
Changes the node's internal type tag, forwarding through references when possible. Switching to map type clears children, resets list state, and resets the array index.
Prefer the typed setters (`set`, `set_bool`, `set_array`) in normal unit code.
:example
DValue v;
v.set_array();
DValue item; item.set("x"); v.push(item);
print("type after set_array: ", v.get_type_name(), "\n");

View File

@ -0,0 +1,19 @@
:sig
bool DValue::to_bool(bool default_value = false) const
:params
bool : fallback returned when conversion is not possible
return value : converted value or the fallback
:see
>types
0_DValue
2_DValue_get_by_path
:content
Converts strings, numbers, bools, pointers, and maps to bool. Text such as `true`, `yes`, `on`, `1`, `false`, `no`, `off`, `0`, and `null` is recognized; non-empty unparseable strings are truthy; empty strings use the default. Multi-child maps are true when non-empty.
:example
DValue value;
value = "true";
print(value.to_bool() ? "true\n" : "false\n");

View File

@ -0,0 +1,19 @@
:sig
f64 DValue::to_f64(f64 default_value = 0) const
:params
f64 : fallback returned when conversion is not possible
return value : converted value or the fallback
:see
>types
0_DValue
2_DValue_get_by_path
:content
Converts strings, f64, bool, pointer, and single-value maps to a finite double. Invalid or empty strings return the default.
:example
DValue value;
value = "12.5";
print(value.to_f64(), "\n");

View File

@ -0,0 +1,19 @@
:sig
String DValue::to_json(char quote_char = '"') const
:params
quote_char : quote character passed to string escaping
return value : JSON-ish scalar representation
:see
>types
0_DValue
json_encode
:content
Returns the JSON scalar representation used by this low-level method: strings are escaped and quoted, numbers are emitted with `std::to_string`, bools as `true`/`false`, maps as `"(array)"`, pointers as `"(pointer)"`, and references as `"(reference)"`. For full structured encoding, use the runtime JSON helpers.
:example
DValue value;
value = "Ada";
print(value.to_json(), "\n");

View File

@ -0,0 +1,19 @@
:sig
s64 DValue::to_s64(s64 default_value = 0) const
:params
s64 : fallback returned when conversion is not possible
return value : converted value or the fallback
:see
>types
0_DValue
2_DValue_get_by_path
:content
Converts strings, f64, bool, pointer, and single-value maps to a signed integer. Invalid or empty strings return the default. Values outside the signed range are clamped.
:example
DValue value;
value = "-42";
print(value.to_s64(), "\n");

View File

@ -0,0 +1,19 @@
:sig
String DValue::to_string(String default_value = "") const
:params
String : fallback returned when conversion is not possible
return value : converted value or the fallback
:see
>types
0_DValue
2_DValue_get_by_path
:content
Returns the stored string, converts f64/bool/pointer values to text, and returns the default for empty strings, maps, unresolved references, or unknown types. Bool text is `(true)` or `(false)`.
:example
DValue value;
value = true;
print(value.to_string(), "\n");

View File

@ -0,0 +1,20 @@
:sig
StringMap DValue::to_stringmap() const
:params
return value : StringMap copy of the value
:see
>types
0_DValue
0_StringMap
2_DValue_to_string
:content
Converts a `DValue` to `StringMap`. Map children become entries converted with `to_string()`. Non-empty strings become `value=<string>`. f64, bool, and pointer values become `value=<to_string()>`. References that cannot be resolved produce an empty map.
:example
DValue user;
user["name"] = "Ada";
StringMap fields = user.to_stringmap();
print(fields["name"], "\n");

View File

@ -0,0 +1,19 @@
:sig
u64 DValue::to_u64(u64 default_value = 0) const
:params
u64 : fallback returned when conversion is not possible
return value : converted value or the fallback
:see
>types
0_DValue
2_DValue_get_by_path
:content
Converts strings, f64, bool, pointer, and single-value maps to an unsigned integer. Invalid or empty strings return the default. Negative values clamp to zero and oversized values clamp to `u64` max.
:example
DValue value;
value = "42";
print(value.to_u64(), "\n");

View File

@ -0,0 +1,21 @@
:sig
DValue DValue::values() const
:params
return value : a list-shaped DValue containing each iterated child value
:see
>types
0_DValue
2_DValue_each
2_DValue_push
:content
Copies the values produced by `each()` into a new list-shaped `DValue`.
:example
DValue user;
user["name"] = "Ada";
user["role"] = "admin";
DValue values = user.values();
print(values.is_list() ? "list\n" : "map\n");

View File

@ -0,0 +1,20 @@
:sig
void Request::ob_start()
:params
return value : none
:see
0_Request
ob_start
ob_get_close
:content
Starts a new output buffer for the request and makes it the active `context.ob`. Most unit code should prefer the higher-level output-buffer helpers (`ob_start()`, `ob_get()`, `ob_get_close()`, `ob_close()`) rather than calling the method directly.
:example
context.ob_start();
print("fragment");
String captured = ob_get_close();
print("captured: ", captured, "\n");

View File

@ -0,0 +1,20 @@
:sig
void Request::set_status(s32 code, String reason = "")
:params
code : HTTP status code
reason : optional reason phrase override
return value : none
:see
>types
0_Request
redirect
set_cookie
:content
Sets the request status line and mirrors the numeric status into `context.flags.status`. When `reason` is omitted, common HTTP status codes get their standard phrase. FastCGI-style requests use a `Status:` prefix; other requests use `HTTP/1.1`.
:example
context.set_status(200, "OK");
print(context.flags.status, "\n");

View File

@ -0,0 +1,19 @@
:sig
template<typename F> void StringList::each(F f) const
:params
f : callback called with each string
return value : none
:content
Calls the callback once for each string in order. Use it when you want side effects such as building output or accumulating a separate value.
:example
StringList items = split("a,b,c", ",");
items.each([](String item) { print(item, "\n"); });
:see
>types
0_StringList
2_StringList_map
2_StringList_keys

View File

@ -0,0 +1,21 @@
:sig
template<typename F> bool StringList::every(F f) const
:params
f : predicate called with each string
return value : `true` when every item matches
:content
Tests whether all strings in the list satisfy the predicate. Use it for validation checks where a list is acceptable only if every member passes the same rule.
:example
StringList routes = split("dashboard,index,themes", ",");
bool all_named = routes.every([](String item) { return(item != ""); });
print(all_named ? "all named\n" : "missing name\n");
:see
>types
0_StringList
2_StringList_some
2_StringList_find
2_StringList_filter

View File

@ -0,0 +1,21 @@
:sig
template<typename F> StringList StringList::filter(F f) const
:params
f : predicate called with each string
return value : new list containing items where `f(item)` is true
:content
Returns a new `StringList` containing the original items for which the predicate returns true. Order is preserved.
:example
StringList routes = split("home,admin,docs", ",");
StringList visible = routes.filter([](String item) { return(item != "admin"); });
print(join(visible, ","), "\n");
:see
>types
0_StringList
2_StringList_map
2_StringList_some
2_StringList_every

View File

@ -0,0 +1,22 @@
:sig
template<typename F> String StringList::find(F f, String fallback = "") const
:params
f : predicate called with each string
fallback : value returned when no item matches
return value : the first matching string, or `fallback`
:content
Returns the first string in the list that satisfies the predicate. Use it when you need the matching value itself, not just a yes/no answer, and when a clear fallback is better than a separate missing-value branch.
:example
StringList routes = split("dashboard,index,themes", ",");
String route = routes.find([](String item) { return(str_starts_with(item, "them")); }, "missing");
print(route, "\n");
:see
>types
0_StringList
2_StringList_some
2_StringList_every
2_StringList_filter

View File

@ -0,0 +1,18 @@
:sig
StringList StringList::keys() const
:params
return value : index keys as strings
:content
Returns the list indexes as strings from `0` through `size() - 1`. Use it when code expects string keys for a list-shaped value.
:example
StringList items = split("red,green,blue", ",");
print(join(items.keys(), ","), "\n");
:see
>types
0_StringList
2_StringList_each
2_StringList_map

View File

@ -0,0 +1,20 @@
:sig
template<typename F> StringList StringList::map(F f) const
:params
f : mapper called with each string
return value : new list of mapped strings
:content
Returns a new `StringList` containing the mapper result for each item, in the same order. Use it for simple string transformations while keeping list shape.
:example
StringList names = split("ada,grace", ",");
StringList upper = names.map([](String item) { return(to_upper(item)); });
print(join(upper, ","), "\n");
:see
>types
0_StringList
2_StringList_filter
2_StringList_each

View File

@ -0,0 +1,21 @@
:sig
template<typename F> bool StringList::some(F f) const
:params
f : predicate called with each string
return value : `true` when at least one item matches
:content
Tests whether any string in the list satisfies the predicate. Use it for concise presence checks when the condition is more specific than direct equality.
:example
StringList routes = split("dashboard,index,themes", ",");
bool has_index = routes.some([](String item) { return(item == "index"); });
print(has_index ? "yes\n" : "no\n");
:see
>types
0_StringList
2_StringList_every
2_StringList_find
2_StringList_filter

View File

@ -0,0 +1,19 @@
:sig
StringList StringList::sort() const
:params
return value : a new `StringList` sorted in ascending string order
:content
Returns a sorted copy of the list without changing the original. Use it for deterministic output, menus, manifests, and test comparisons.
:example
StringList names = split("beta,alpha,gamma", ",");
StringList sorted = names.sort();
print(join(sorted, ","), "\n");
:see
>types
0_StringList
2_StringList_unique
join

View File

@ -0,0 +1,20 @@
:sig
StringList StringList::unique() const
:params
return value : a new `StringList` containing each distinct string once
:content
Returns a copy of the list with duplicate strings removed. The first occurrence of each string is kept, so the result preserves first-seen order.
:example
StringList routes = split("dashboard,index,dashboard,themes", ",");
StringList unique_routes = routes.unique();
print(join(unique_routes, ","), "\n");
:see
>types
0_StringList
2_StringList_sort
2_StringList_find
join

View File

@ -0,0 +1,57 @@
:title
Blocked functions (hostcall blocklist)
:content
UCE units reach the operating system only through a fixed set of `uce_host_*`
membrane hostcalls (see the runtime architecture). A server operator can
**disable individual hostcalls** so a deployment exposes only the capabilities it
wants — for example turning off `shell_exec` or `http_request` on a hardened
host. A unit that calls a disabled function fails at request time with the
configurable error page, stating exactly which function was blocked and why.
## Configuration
Set `UCE_HOSTCALL_BLOCKLIST` in `/etc/uce/settings.cfg` to a comma-separated list
of hostcall names. Names may be given bare (`shell_exec`) or fully qualified
(`uce_host_shell_exec`); whitespace is ignored. Empty (the default) blocks
nothing.
Changes take effect on **restart** (`systemctl restart uce`). There is no hot
reload — the list is parsed once per worker process into a fast lookup, so an
empty list has zero runtime cost and a non-empty list costs only a single check
per hostcall at workspace birth (never per call).
## Behaviour when a blocked function is called
The blocked hostcall resolves to a trap stub instead of its real implementation.
When a unit invokes it, the request fails into the runtime error page with:
- error type `policy_blocked` (so a custom error page template can special-case it),
- a title `function disabled by server policy`,
- a message naming the exact function, e.g. *"this unit called uce_host_shell_exec,
which is disabled on this server by configuration (UCE_HOSTCALL_BLOCKLIST)"*.
The worker is unharmed (it is a clean guest trap, like any other), and only the
offending request fails. A unit cannot catch this — blocking is enforcement, not
a soft signal.
## What can and cannot be blocked
Any `uce_host_*` capability hostcall can be listed — file I/O, `shell_exec` /
`shell_spawn`, `http_request`, `mysql`, sockets, memcache, crypto, the job
registry, etc. (the full set is the membrane list in the runtime architecture
doc and `src/wasm/core_hostcalls.syms`).
A small core set the runtime itself needs is **exempt** and ignored even if
listed, so a deployment cannot be bricked by an over-broad blocklist:
`component_resolve` (used for `component()` / unit rendering).
## Notes
- No recompilation is required (neither the wasm core nor the native binary) —
this is pure runtime configuration; blocked hostcalls still exist as imports,
they simply resolve to a trap.
- Pure-compute library functions that are NOT hostcalls (string ops, `DValue`
methods, hashing helpers like `gen_noise`, etc.) are not OS capabilities and
cannot be blocked this way — only `uce_host_*` membrane calls are gateable.

View File

@ -63,84 +63,29 @@ For a source file like `/some/path/page.uce`, the preprocessor produces:
Literal output with escaped data:
```cpp
RENDER(Request& context)
{
<><h1><?= context.params["DOCUMENT_URI"] ?></h1></>
}
```
The same thing can also be written with PHP-style literal delimiters:
```cpp
RENDER(Request& context)
{
?><h1><?= context.params["DOCUMENT_URI"] ?></h1><?
}
```
Roughly becomes:
```cpp
print(R"(<h1>)");
print(html_escape(context.params["DOCUMENT_URI"]));
print(R"(</h1>)");
```
Literal output with trusted unescaped markup:
```cpp
RENDER(Request& context)
{
<><div class="panel"><?: component("components/card", context.props, context) ?></div></>
}
```
Roughly becomes:
```cpp
print(R"(<div class="panel">)");
print(component("components/card", context.props, context));
print(R"(</div>)");
```
Compile-time composition:
```cpp
#load "partials/nav.uce"
RENDER(Request& context)
{
<><body>...</body></>
}
```
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>`.
@ -173,7 +118,9 @@ The page template can then render `context.call["fragments"]["head"]` inside `<h
- 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
- PHP: template tags like `<?php ... ?>`, `<?= ... ?>`, output buffering, and compile-time include patterns
- JavaScript / Node.js: JSX transforms, tagged templates, server-side rendering pipelines, and build-time HTML generation
:example
// The preprocessor lets you mix C++ logic with output. Code generates markup:
StringList items = split("apples,pears,plums", ",");
String html = "";
items.each([&](String item) { html += "<li>" + item + "</li>"; });
print(html, "\n");

View File

@ -12,7 +12,8 @@ unit_render
3_C++ Preprocessor
map
filter
dv_filter
2_DValue_filter
2_DValue_map
:content
UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly.
@ -44,12 +45,6 @@ That keeps file-based and hierarchical routing in normal UCE code instead of hid
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)); });
DValue app_items = dv_filter(menu, [](DValue item, String key) { return(item["section"].to_string() == "app"); });
DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); });
```
Use these when a short transformation is clearer than a loop. Prefer explicit loops for side effects or multi-step validation.
@ -67,3 +62,7 @@ When a unit fails to compile, UCE reports the source path, generated C++ path, c
- No global file-router is imposed by the runtime.
- No JSX-like component tags are required for this workflow.
- Component children/slot syntax is not part of UCE yet; use explicit props and component calls for now.
:example
// Components are UCE's reusable, props-driven building blocks (like React components).
print(component("examples/sample_unit"), "\n");

View File

@ -0,0 +1,23 @@
:sig
Doc pages are text files in site/doc/pages/*.txt using colon-prefixed sections.
:params
:sig : one or more real C++ signatures, exactly as declared
:params : `name : description` rows plus `return value : description`
:content : usage-first prose in 2-5 tight sentences
:example : runnable UCE code whose real output is captured by the renderer
:see : an area reference (`>area`) followed by closest sibling page slugs
:content
Doc pages use a small structured text format so the index, detail view, search, and tests can all reason about the same source. Put usage first: describe what the API does and when to reach for it before lower-level implementation notes. Every page should include at least one deterministic `:example`; the renderer materializes each example as a temporary UCE unit, compiles it, renders it, and shows both the source and captured output. Keep cross-membrane, lock, and PHP/JS equivalence notes short and trailing when they are useful.
:example
print("A doc page normally contains :sig, :params, :content, :example, and :see.\n");
print("Examples must print deterministic output.\n");
:see
>runtime
unit_compile
unit_render
ob_start
ob_get_close

View File

@ -1,25 +0,0 @@
: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

View File

@ -10,7 +10,7 @@ return value : merged result
:see
>types
0_DValue
StringMap
0_StringMap
json_decode
:content
@ -22,7 +22,10 @@ For `DValue`, string keys from `b` overwrite keys from `a`. Numeric keys are app
This helper is the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data.
## Related Concepts
- PHP: `array_merge()`
- JavaScript / Node.js: object spread, `Object.assign()`, or array concatenation depending on whether the data is map-like or list-like
:example
StringMap left;
left["name"] = "Ada";
StringMap right;
right["role"] = "admin";
StringMap merged = array_merge(left, right);
print(merged["name"], " ", merged["role"], "\n");

View File

@ -2,18 +2,15 @@
String ascii_safe_name(String raw)
:params
raw : input string to normalize
return value : ASCII-safe identifier made from letters, digits, and underscores
raw : arbitrary input string
return value : an ASCII-only sanitized name
:content
Like `safe_name()`, but also restricts the result to ASCII, transliterating or dropping non-ASCII characters. Use it when the consumer (a legacy filesystem, header, or protocol) requires plain ASCII.
:example
print(ascii_safe_name("Cafe Munchen 2024"), "\n");
:see
>string
:content
Builds a conservative identifier by keeping ASCII letters, digits, and underscores and dropping other characters.
This is useful when turning user- or config-provided names into handler suffixes, DOM-safe variable stems, or CSS and JS hook names.
## Related Concepts
- PHP: slug or safe-filename helpers built with `preg_replace()` and transliteration utilities
- JavaScript / Node.js: regex-based slugify or safe-name helpers used for URLs and filenames
safe_name

View File

@ -1,5 +1,5 @@
:sig
String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0)
String backtrace_capture(u32 max_frames = 32, u32 skip_frames = 0)
:params
max_frames : maximum number of frames to capture
@ -8,7 +8,7 @@ return value : formatted backtrace string
:see
>runtime
backtrace_frames_string
backtrace_get_frames
signal_name
:content
@ -18,7 +18,7 @@ This helper is for diagnostics. It is used by runtime error reporting paths and
Example:
```uce
String trace = capture_backtrace_string(16, 0);
print("<pre>", html_escape(trace), "</pre>");
```
:example
String trace = backtrace_capture();
print(trace == "" ? "(empty in the wasm sandbox; populated by the native crash handler)" : trace, "\n");

View File

@ -1,18 +0,0 @@
:sig
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames = 0)
:params
frames : frame pointer array returned by native backtrace collection
size : number of frames in the array
skip_frames : number of newest frames to omit
return value : formatted backtrace string
:see
>runtime
capture_backtrace_string
signal_name
:content
Formats a captured native backtrace frame array.
Most page code should use `capture_backtrace_string()` instead. Use this helper when you already have raw frame pointers from lower-level diagnostic code.

View File

@ -0,0 +1,21 @@
:sig
String backtrace_get_frames(void* const* frames, size_t size, u32 skip_frames = 0)
:params
frames : frame pointer array returned by native backtrace collection
size : number of frames in the array
skip_frames : number of newest frames to omit
return value : formatted backtrace string
:see
>runtime
backtrace_capture
signal_name
:content
Formats a captured native backtrace frame array.
Most page code should use `backtrace_capture()` instead. Use this helper when you already have raw frame pointers from lower-level diagnostic code.
:example
print(backtrace_get_frames(0, 0) == "" ? "(no frames available in the wasm sandbox)" : "frames captured", "\n");

View File

@ -1,30 +1,16 @@
:sig
String base64_decode(String raw, bool& ok)
String base64_decode(String raw)
:params
raw : Base64 encoded string
ok : set to `true` when decoding succeeds; set to `false` for invalid input
return value : decoded binary-safe string, or an empty string when decoding fails
:see
>string
base64_encode
raw : Base64-encoded text
return value : the decoded bytes (empty on invalid input)
:content
Decodes a Base64 string.
Decodes Base64 text back to bytes. Pairs with `base64_encode()` for round-tripping binary data through text channels.
Pass a `bool` variable for `ok` so callers can distinguish invalid input from a valid empty decoded value.
:example
print(base64_decode("aGVsbG8="), "\n");
Example:
```uce
bool ok = false;
String decoded = base64_decode("aGVsbG8=", ok);
// ok == true
// decoded == "hello"
```
Related:
- PHP: `base64_decode($value, true)`
- JavaScript / Node.js: `Buffer.from(value, "base64")`
:see
>uri
base64_encode

View File

@ -2,26 +2,16 @@
String base64_encode(String raw)
:params
raw : binary-safe source string
return value : Base64 encoded string
:see
>string
base64_decode
raw : binary-safe source bytes
return value : the Base64-encoded text
:content
Encodes a string with Base64.
Encodes bytes as Base64 text. UCE strings are binary-safe, so `raw` may contain NUL and other non-text bytes — handy for embedding `random_bytes()` or a `sha256()` digest in headers, cookies, or JSON.
UCE strings can contain binary data, so `raw` may include NUL bytes and non-text bytes.
:example
print(base64_encode("hello"), "\n");
Example:
```uce
String encoded = base64_encode("hello");
// encoded == "aGVsbG8="
```
Related:
- PHP: `base64_encode()`
- JavaScript / Node.js: `Buffer.from(value).toString("base64")`
:see
>uri
base64_decode
random_bytes

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