From 7066da3cdef08b01dda64bca34caadc7000587b2 Mon Sep 17 00:00:00 2001 From: udo Date: Fri, 12 Jun 2026 11:05:52 +0000 Subject: [PATCH] refactor: rename DTree to DValue --- README.md | 10 +- RECOMMENDATIONS.md | 38 +-- WASM-PROPOSAL.md | 143 +++++------ docs/react-developer-affordances-todo.md | 6 +- scripts/refactor/rename-dvalue-api.py | 118 ++++++++++ site/demo/call_file_funcs.uce | 2 +- site/demo/collections.uce | 14 +- site/demo/components.uce | 4 +- site/demo/demo_guard.h | 2 +- site/demo/{dtree.uce => dvalue.uce} | 32 +-- site/demo/file_append.uce | 2 +- site/demo/fileio.uce | 2 +- site/demo/header.uce | 2 +- site/demo/index.uce | 8 +- site/demo/json.uce | 4 +- site/demo/markdown.uce | 4 +- site/demo/memcached.uce | 2 +- site/demo/once-init.uce | 6 +- site/demo/regex.uce | 6 +- site/demo/sqlite.uce | 4 +- site/demo/task.uce | 2 +- site/demo/task_repeat.uce | 2 +- site/demo/unit-browser.uce | 4 +- site/demo/unit-info.uce | 6 +- site/demo/websockets.ws.uce | 8 +- site/demo/xml.uce | 18 +- site/demo/yaml.uce | 14 +- site/demo/zip.uce | 4 +- site/doc/areas/types.txt | 16 +- site/doc/pages/{0_DTree.txt => 0_DValue.txt} | 26 +- site/doc/pages/0_Request.txt | 18 +- site/doc/pages/1_CLI.txt | 4 +- site/doc/pages/1_WS.txt | 2 +- site/doc/pages/array_merge.txt | 6 +- site/doc/pages/cli_arg.txt | 2 +- site/doc/pages/cli_input.txt | 8 +- site/doc/pages/coming_from_react.txt | 6 +- site/doc/pages/component.txt | 8 +- site/doc/pages/component_render.txt | 4 +- .../pages/{dtree_filter.txt => dv_filter.txt} | 8 +- .../{dtree_group_by.txt => dv_group_by.txt} | 8 +- .../doc/pages/{dtree_pick.txt => dv_keys.txt} | 10 +- site/doc/pages/{dtree_map.txt => dv_map.txt} | 8 +- .../doc/pages/{dtree_omit.txt => dv_omit.txt} | 10 +- .../doc/pages/{dtree_keys.txt => dv_pick.txt} | 10 +- .../pages/{dtree_values.txt => dv_values.txt} | 10 +- site/doc/pages/each.txt | 18 +- site/doc/pages/error_pages.txt | 2 +- site/doc/pages/get_by_path.txt | 10 +- site/doc/pages/has.txt | 6 +- site/doc/pages/is_array.txt | 10 +- site/doc/pages/is_list.txt | 12 +- site/doc/pages/json_decode.txt | 18 +- site/doc/pages/json_encode.txt | 10 +- site/doc/pages/list_every.txt | 2 +- site/doc/pages/list_find.txt | 2 +- site/doc/pages/list_some.txt | 2 +- site/doc/pages/list_sort.txt | 2 +- site/doc/pages/list_unique.txt | 2 +- site/doc/pages/map.txt | 2 +- site/doc/pages/markdown_to_ast.txt | 14 +- site/doc/pages/markdown_to_html.txt | 4 +- site/doc/pages/mysql_query.txt | 6 +- site/doc/pages/regex_search.txt | 10 +- site/doc/pages/regex_search_all.txt | 12 +- site/doc/pages/request_query_route.txt | 4 +- site/doc/pages/sqlite_query.txt | 14 +- site/doc/pages/to_bool.txt | 6 +- site/doc/pages/to_f64.txt | 6 +- site/doc/pages/to_json.txt | 6 +- site/doc/pages/to_s64.txt | 6 +- site/doc/pages/to_string.txt | 6 +- site/doc/pages/to_stringmap.txt | 12 +- site/doc/pages/to_u64.txt | 8 +- site/doc/pages/unit_call.txt | 14 +- site/doc/pages/unit_info.txt | 2 +- site/doc/pages/var_dump.txt | 4 +- site/doc/pages/xml_decode.txt | 12 +- site/doc/pages/xml_encode.txt | 16 +- site/doc/pages/yaml_decode.txt | 10 +- site/doc/pages/yaml_encode.txt | 14 +- site/doc/pages/zip_create.txt | 8 +- site/doc/pages/zip_list.txt | 8 +- site/errors/compiler-error.uce | 2 +- site/errors/runtime-error.uce | 2 +- .../components/auth/oauth-client.uce | 4 +- .../uce-starter/components/data/widgets.uce | 34 +-- .../components/example/marketing_blocks.uce | 36 +-- .../components/example/theme-switcher.uce | 2 +- .../components/gauges/arcgauge.uce | 10 +- .../uce-starter/components/gauges/helpers.uce | 4 +- .../components/gauges/needlegauge.uce | 12 +- .../components/gauges/progressbar.uce | 22 +- .../components/theme/account_links.uce | 2 +- .../uce-starter/components/theme/assets.uce | 4 +- .../components/theme/standard_nav.uce | 4 +- site/examples/uce-starter/config/settings.uce | 4 +- site/examples/uce-starter/index.uce | 14 +- site/examples/uce-starter/lib/app.uce | 6 +- site/examples/uce-starter/lib/user.class.h | 34 +-- .../uce-starter/themes/dark/page.html.uce | 4 +- .../uce-starter/themes/light/page.html.uce | 2 +- .../themes/localfirst/page.html.uce | 6 +- .../themes/portal-dark/page.html.uce | 4 +- .../themes/portal-light/page.html.uce | 2 +- .../themes/retro-gaming/page.html.uce | 4 +- .../uce-starter/views/account/login.uce | 2 +- .../uce-starter/views/account/profile.uce | 4 +- .../uce-starter/views/account/register.uce | 2 +- site/examples/uce-starter/views/auth/demo.uce | 4 +- .../views/auth/store-oauth-session.uce | 4 +- site/examples/uce-starter/views/dashboard.uce | 14 +- site/examples/uce-starter/views/features.uce | 2 +- site/examples/uce-starter/views/gauges.uce | 4 +- site/examples/uce-starter/views/index.uce | 12 +- site/examples/uce-starter/views/page1.uce | 4 +- .../uce-starter/views/theme-preview.uce | 2 +- site/examples/uce-starter/views/themes.uce | 4 +- .../uce-starter/views/workspace/index.uce | 22 +- site/info/components/code_example.uce | 2 +- site/info/config/settings.uce | 4 +- site/info/index.uce | 6 +- site/tests/call_helpers.uce | 2 +- site/tests/cli.uce | 2 +- site/tests/components.uce | 2 +- site/tests/core.uce | 70 +++--- site/tests/index.uce | 4 +- site/tests/manifest.txt | 4 +- site/tests/markdown.uce | 4 +- site/tests/sqlite.uce | 4 +- site/tests/testlib.h | 4 +- site/tests/units.uce | 2 +- site/tests/websockets.ws.uce | 6 +- site/tests/zip.uce | 12 +- src/fastcgi/src/fcgicc.h | 2 +- src/lib/cli.cpp | 10 +- src/lib/cli.h | 2 +- src/lib/compiler.cpp | 48 ++-- src/lib/compiler.h | 14 +- src/lib/{dtree.cpp => dvalue.cpp} | 222 +++++++++--------- src/lib/{dtree.h => dvalue.h} | 40 ++-- src/lib/functionlib.cpp | 194 +++++++-------- src/lib/functionlib.h | 32 +-- src/lib/markdown.cpp | 174 +++++++------- src/lib/markdown.h | 6 +- src/lib/mysql-connector.cpp | 22 +- src/lib/mysql-connector.h | 10 +- src/lib/sqlite-connector.cpp | 22 +- src/lib/sqlite-connector.h | 10 +- src/lib/types.h | 18 +- src/lib/uce_lib.cpp | 2 +- src/lib/uri.cpp | 8 +- src/lib/uri.h | 4 +- src/lib/zip.cpp | 22 +- src/lib/zip.h | 4 +- src/linux_fastcgi.cpp | 50 ++-- todo.txt | 4 +- 157 files changed, 1203 insertions(+), 1074 deletions(-) create mode 100755 scripts/refactor/rename-dvalue-api.py rename site/demo/{dtree.uce => dvalue.uce} (86%) rename site/doc/pages/{0_DTree.txt => 0_DValue.txt} (73%) rename site/doc/pages/{dtree_filter.txt => dv_filter.txt} (81%) rename site/doc/pages/{dtree_group_by.txt => dv_group_by.txt} (81%) rename site/doc/pages/{dtree_pick.txt => dv_keys.txt} (82%) rename site/doc/pages/{dtree_map.txt => dv_map.txt} (81%) rename site/doc/pages/{dtree_omit.txt => dv_omit.txt} (82%) rename site/doc/pages/{dtree_keys.txt => dv_pick.txt} (82%) rename site/doc/pages/{dtree_values.txt => dv_values.txt} (84%) rename src/lib/{dtree.cpp => dvalue.cpp} (72%) rename src/lib/{dtree.h => dvalue.h} (66%) diff --git a/README.md b/README.md index 5985b59..f149711 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ Useful related runtime patterns: Useful helpers for that data model include: -- `DTree::get_by_path("a/b/c")` for path-style config traversal without creating missing keys -- `DTree::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended -- `DTree::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values +- `DValue::get_by_path("a/b/c")` for path-style config traversal without creating missing keys +- `DValue::has("key")` / `key("key")` for non-mutating child lookup, and `get_or_create("key")` when creation is intended +- `DValue::to_u64()`, `to_s64()`, `to_f64()`, `to_bool()`, and `to_stringmap()` for typed reads from structured values - `json_encode(String)` for emitting JavaScript-safe string literals directly - `ascii_safe_name(String)` for conservative ASCII identifier normalization - `path_join(base, child)` for filesystem-style path assembly @@ -86,7 +86,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()`, `dtree_filter()`, `dtree_map()`, `dtree_pick()`, and related helpers for route/menu/card data shaping near render code +- `map()`, `filter()`, `list_unique()`, `dv_filter()`, `dv_map()`, `dv_pick()`, and related helpers for route/menu/card data shaping near render code Named component handlers are also supported: @@ -186,7 +186,7 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate By default, the WebSocket scope is the current page file, so `ws_send()` queues a message for clients connected to that same `.ws.uce` endpoint. -Each live WebSocket connection owns a broker-side `DTree` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. +Each live WebSocket connection owns a broker-side `DValue` exposed to page code as `context.connection`. Mutations to that tree persist for the life of the socket and are visible on later `WS(Request& context)` calls for the same client. The current inbound payload is available directly as `context.in`, and the runtime mirrors message metadata into `context.params` using keys such as `WS_CONNECTION_ID`, `WS_SCOPE`, `WS_CONNECTION_COUNT`, `WS_OPCODE`, `WS_MESSAGE_TYPE`, and `WS_DOCUMENT_URI`. diff --git a/RECOMMENDATIONS.md b/RECOMMENDATIONS.md index 97adbdc..592c104 100644 --- a/RECOMMENDATIONS.md +++ b/RECOMMENDATIONS.md @@ -25,7 +25,7 @@ candidates. Status legend: The island component emits JSON props inside a single-quoted attribute (`data-props=''`), but `html_escape()` (`src/lib/functionlib.cpp:936`) escapes only `& < > "`, and `json_encode` / -`json_escape` (`src/lib/dtree.cpp:794`) never escape apostrophes. Any `'` in a +`json_escape` (`src/lib/dvalue.cpp:794`) never escape apostrophes. Any `'` in a prop value terminates the attribute. **Failure:** a view passes user-influenced prop text containing an apostrophe, @@ -193,21 +193,21 @@ correctly) and use it in both places. **Status:** fixed, but fix not verified yet. -### 1.10 O(n²) and per-row deep copies in dtree_map / dtree_filter +### 1.10 O(n²) and per-row deep copies in dv_map / dv_filter **File:** `src/lib/functionlib.cpp:182` Both helpers call `tree.is_list()` inside the per-element callback — -`is_list()` (`src/lib/dtree.cpp:165`) walks the entire map, making the +`is_list()` (`src/lib/dvalue.cpp:165`) walks the entire map, making the operation O(n²) — even though both already compute `is_list()` once before -the loop. Additionally, `DTree::each` (`dtree.h:22`) takes the callback -element by value (`DTree t`), deep-copying every subtree per iteration. +the loop. Additionally, `DValue::each` (`dvalue.h:22`) takes the callback +element by value (`DValue t`), deep-copying every subtree per iteration. **Failure:** mapping over a 1000-row `sqlite_query` result does ~1M key-validation checks plus a full deep copy of each row tree. **Fix:** hoist `is_list()` into a `bool` local reused in the callback; change -`each()` to pass `const DTree&`. +`each()` to pass `const DValue&`. **Status:** fixed, but fix not verified yet. @@ -321,7 +321,7 @@ lies. The router rewrite adds candidate `kind`/`matched` fields and `context.call["route"]["candidates"]/["resolved"]/["missed"]`, none of which -is read by any component, theme, or test. It also uses `dtree_filter` to +is read by any component, theme, or test. It also uses `dv_filter` to `file_exists`-stat every candidate after the first match is already found. ~55 lines plus a builder helper replace what the deleted `app_resolve_view` did with three early-return `file_exists` checks. @@ -512,11 +512,11 @@ verified live against the dev server (network suite: 21/21 public tests pass). > handle and signals failure via `_preload_next_error_code`, and `query()` / > `error()` guard against a null connection (clean error message instead of > the SIGSEGV this otherwise caused). -> - The 7.8 nit about the `assets.uce` lambda taking `DTree` by value is -> withdrawn: `DTree::to_string()` is non-const, so a `const DTree&` parameter -> cannot call it. Making the DTree read accessors const-correct would be the +> - The 7.8 nit about the `assets.uce` lambda taking `DValue` by value is +> withdrawn: `DValue::to_string()` is non-const, so a `const DValue&` parameter +> cannot call it. Making the DValue read accessors const-correct would be the > real fix and is a separate, larger change (worth doing before the WASM -> DTree C ABI freezes the surface). +> DValue C ABI freezes the surface). ### Verified clean @@ -524,7 +524,7 @@ verified live against the dev server (network suite: 21/21 public tests pass). **1.4** (multi-statement rejection is comment/whitespace-tolerant, test), **1.9/5.5** (`compiler_generated_cpp_path` agrees with `setup_unit_paths`; confirmed live — the error page now prints the real `/tmp/uce/work/...` path), -**1.10** (`each`/`push` take `const DTree&`, `is_list()` hoisted), +**1.10** (`each`/`push` take `const DValue&`, `is_list()` hoisted), **1.11** (both the initial scan and the loop body are wrapped), **2.1, 2.2, 2.3, 2.5/3.4, 3.1, 3.2, 3.5, 4.1, 4.2, 4.4, 4.5** (SQLITE_STATIC is safe: the params map outlives the statement), **5.3, 5.4**. @@ -655,8 +655,8 @@ startup so libgcc's lazy init doesn't allocate on first use either. - `MySQL::error()` consumes/clears state in the new `statement_info` branch but not in the errno-switch branch, and `SQLite::error()` never consumes — pick one semantic. -- `theme/assets.uce` lambda still takes `DTree item` by value; make it - `const DTree&` to match the new `each()` signature. +- `theme/assets.uce` lambda still takes `DValue item` by value; make it + `const DValue&` to match the new `each()` signature. - `__UceFragmentCapture` is regenerated inline in every captured entry point; define the struct once in the runtime headers and have the prelude emit only the instantiation line (less generated-code surface — also smaller wasm unit @@ -675,11 +675,11 @@ startup so libgcc's lazy init doesn't allocate on first use either. ### 7.9 NEW: lists with ten or more entries iterate in lexicographic, not numeric, order -**File:** `src/lib/dtree.cpp` (`DTree::each`), found while documenting the accessors (2026-06-11) +**File:** `src/lib/dvalue.cpp` (`DValue::each`), found while documenting the accessors (2026-06-11) -`DTree` stores list entries as string keys in a `std::map`, so iteration order +`DValue` stores list entries as string keys in a `std::map`, so iteration order is lexicographic: `"0", "1", "10", "11", "2", ...`. Every consumer of `each()` -inherits this — `dtree_map`/`dtree_filter`/`dtree_values` re-push in that +inherits this — `dv_map`/`dv_filter`/`dv_values` re-push in that order and therefore *scramble* any `push()`-built list with ≥ 10 entries, and a `sqlite_query()` result with ≥ 10 rows renders rows 10+ before row 2 when iterated. `is_list()` still reports true because the key *set* is sequential. @@ -687,7 +687,7 @@ iterated. `is_list()` still reports true because the key *set* is sequential. **Fix options:** iterate numerically in `each()` when `is_list()` is true (cheapest, fixes all consumers at once); or use a numeric-aware comparator in the map type. Documented honestly on the `each` doc page in the meantime. -This also needs deciding before the WASM DTree C ABI freezes iteration-order +This also needs deciding before the WASM DValue C ABI freezes iteration-order semantics. **Status:** fixed — and the regression test exposed the deeper layer: @@ -698,7 +698,7 @@ serialize big lists as objects instead of arrays. `is_list()` now does an order-independent check (n unique canonical index keys with max n-1), index keys require canonical form (`"1"`, not `"01"`), and `each()` iterates lists in numeric index order, matching the encoders. Regression test with a -12-entry list in core.uce covers `each`, `dtree_values`, and `dtree_map`. +12-entry list in core.uce covers `each`, `dv_values`, and `dv_map`. ### 7.10 NEW: fault recovery left the worker in the crashed unit's working directory diff --git a/WASM-PROPOSAL.md b/WASM-PROPOSAL.md index a5ced4c..3c32287 100644 --- a/WASM-PROPOSAL.md +++ b/WASM-PROPOSAL.md @@ -1,12 +1,11 @@ # WASM-PROPOSAL: WebAssembly Unit Runtime for UCE -- **Status:** proposal / design draft (2026-06-11) +- **Status:** updated design guide (2026-06-12) - **Scope:** replace the native unit pipeline (generated C++ → clang → `.so` → `dlopen`) with per-unit WebAssembly modules executed in a per-request, - runtime-linked workspace, exposing the same API surface to page code. -- **Origin:** design discussion 2026-06-11; incorporates the post-mortem of the - earlier per-invocation arena attempt (preserved in - `src/lib/_scratchpad.cpp`). + runtime-linked workspace, exposing the same API surface to page code, + enabling memory safety, better execution control, and paving the way for + supporting more source languages in the future. --- @@ -26,7 +25,7 @@ with the runtime.** system-allocated objects released mid-request leak. The lifetime distinction lives in the type system and call graph, not at the allocator boundary — fixing it in-process means threading PMR allocators through - `String`, `DTree`, and every container. + `String`, `DValue`, and every container. 2. **Fault recovery is best-effort, not sound.** The current SIGSEGV → `sigsetjmp`/`siglongjmp` recovery performs no unwinding, skips @@ -57,15 +56,17 @@ patching around it: first-class limits (linear-memory cap = RAM limit, epoch/fuel = CPU timeout), per-request memory stats for free. -**Accepted costs:** ~1.2–2× compute slowdown vs. native (still far ahead of +**Expected outcome:** ~1.2–2× compute slowdown vs. native (still far ahead of interpreted runtimes); a real ABI/membrane design; ownership of a custom -loader; toolchain rough edges (§10). +loader; toolchain rough edges (§10). More isolated, shared-nothing PHP architecture +but with a good story about websockets, generic sockets, background tasks, +and other long running processes. --- ## 2. Rejected alternatives (recorded so they stay rejected) -- **In-process PMR arena.** Requires re-typedefing `String`/`DTree` and +- **In-process PMR arena.** Requires re-typedefing `String`/`DValue` and threading allocators through the entire codebase; the failed global-swap shortcut is the only cheap version and it is unsound (§1.1). - **One instance per component.** UCE components are function calls, not @@ -127,11 +128,11 @@ host process (linux_fastcgi, per worker) ### 3.2 Memory model -- **One heap, one allocator, one DTree implementation** — all owned by the +- **One heap, one allocator, one DValue implementation** — all owned by the core module. Unit modules *import* `malloc`/`free`/runtime symbols via GOT; the loader **rejects any unit module that defines rather than imports them** (two allocators on one heap is the one fatal misconfiguration). -- **Bump allocator option.** Because the workspace heap is dropped wholesale, +- **Arena workspace allocator.** Because the workspace heap is dropped wholesale, the core's allocator may be a bump allocator with no-op free — the `_scratchpad.cpp` design, now correct by construction. Per-deployment flag; fallback is wasi-libc dlmalloc. Memory stats = heap pointer − base @@ -150,7 +151,8 @@ Exactly three currencies cross between host and workspace: 1. **scalars** (i32/i64/f64), 2. **byte buffers** (`ptr+len` into linear memory; inbound buffers are - placed via the core's exported allocator), + placed via the core's exported allocator), on the C++ side we strictly + prefer the binary-safe std::string as a container for buffers 3. **handles** (opaque `u32` indices into the per-workspace host handle table; each entry carries a closer callback). @@ -158,22 +160,27 @@ Everything pointer-shaped stays on its own side. Hostcall surface budget: 30–60 functions (§5.1). Host errors return as error values; traps are reserved for unit faults. Nothing throws across the membrane. -**DTrees cross the membrane only as the versioned wire encoding** (§5.3), -at exactly three sites: request context in (once), response out (once), -bulk I/O results in (per query, optional vs. cursor-style hostcalls). +**DValues cross the membrane only as the versioned wire encoding** (§5.3), +at the following sites: request context in (once), response out (once), +some hostcalls. -### 3.4 DTree inside the workspace: no serialization, ever +It is expected that many if not most of the toolset API functions we +expose to the unit developer will be judiciously split across the membrane: +have a relatively minimal wasm part that calls into the runtime host, and +then the runtime implementation which does most of the work. + +### 3.4 DValue inside the workspace: no serialization, ever Within a workspace, all modules share one address space, one toolchain, one set of headers — the C/C++ ABI is intact across module boundaries. Therefore: -- A `DTree` is a pointer (an i32 offset into linear memory). +- A `DValue` is a pointer (an i32 offset into linear memory). - `component(path, context)` resolves path → table index (host registry or guest-resident map) and `call_indirect`s, passing the context pointer. Reference semantics, mutation visibility, shared ob stack, working `ONCE` — identical to today. - Function pointers are shared-table indices, valid across modules: virtual - calls, `std::function` callbacks (`dtree_map` lambdas) work across units. + calls, `std::function` callbacks (`dv_map` lambdas) work across units. - Cost: one `call_indirect` (single-digit ns) + GOT loads for cross-module symbols — the same shape of overhead native PIC pays through the PLT/GOT, i.e. what dlopened `.so` units pay today. @@ -181,10 +188,10 @@ set of headers — the C/C++ ABI is intact across module boundaries. Therefore: Encode/decode is **not** part of internal component calls. It exists only at the membrane (§3.3) and on the cross-instance plane (§4). -### 3.5 The DTree C ABI (load-bearing, build it first) +### 3.5 The DValue C ABI (load-bearing, build it first) The stable contract of the workspace is a **C ABI**, not the C++ class: -the core exports `extern "C"` accessors over an opaque `uce_dtree*` (§5.2), +the core exports `extern "C"` accessors over an opaque `uce_dvalue*` (§5.2), plus the string/ob/print helpers. C++ units may bypass it and use the class directly (same headers, zero cost — a private fast path). Every other workspace language uses the C surface. @@ -210,16 +217,12 @@ PIC linear-memory module that adopts a foreign allocator?* - Must adopt the core allocator (Rust: `#[global_allocator]`; Zig: allocator parameter) and must not unwind across boundaries (`panic=abort` / catch-at-edge). -- Access DTrees through the C ABI: pointer semantics, no copies, ns-scale - calls. Idiomatic wrappers per language (e.g. Rust `DTree<'request>` — +- Access DValues through the C ABI: pointer semantics, no copies, ns-scale + calls. Idiomatic wrappers per language (e.g. Rust `DValue<'request>` — the borrow checker enforces the arena invariant). **Plane B — runtime-carrying languages** (JS, Python, Go, C#, …): -- Their GC/runtime owns its memory; they run as **separate instances** - within the request and communicate through the host using the wire - encoding and handles. Bindings choose per-access hostcalls or bulk - subtree hydration into native dicts/objects — a tuning decision, not an - architectural fork. +- No support currently planned. **The semantic rule (enforced by the loader, not by convention):** cross-plane components do **not** receive the mutable context. They get an @@ -260,25 +263,25 @@ Conventions: all errors as result codes + `uce_host_last_error(buf)`; inbound buffers placed via the core's exported `uce_alloc`; no hostcall traps on bad input (clamp/error instead). -### 5.2 DTree C ABI (core exports; sketch) +### 5.2 DValue C ABI (core exports; sketch) ```c -typedef struct uce_dtree uce_dtree; // opaque; workspace-owned +typedef struct uce_dvalue uce_dvalue; // opaque; workspace-owned -uce_dtree* uce_dtree_root(void); // request context -uce_dtree* uce_dtree_get(uce_dtree*, const char* key, size_t klen); // create-on-write -uce_dtree* uce_dtree_find(uce_dtree*, const char* key, size_t klen); // NULL if absent -const char* uce_dtree_value(uce_dtree*, size_t* len); -void uce_dtree_set_value(uce_dtree*, const char* v, size_t vlen); -size_t uce_dtree_count(uce_dtree*); -int uce_dtree_is_list(uce_dtree*); +uce_dvalue* uce_dv_root(void); // request context +uce_dvalue* uce_dv_get(uce_dvalue*, const char* key, size_t klen); // create-on-write +uce_dvalue* uce_dv_find(uce_dvalue*, const char* key, size_t klen); // NULL if absent +const char* uce_dv_value(uce_dvalue*, size_t* len); +void uce_dv_set_value(uce_dvalue*, const char* v, size_t vlen); +size_t uce_dv_count(uce_dvalue*); +int uce_dv_is_list(uce_dvalue*); /* iteration */ -uce_dtree_iter uce_dtree_iter_begin(uce_dtree*); -int uce_dtree_iter_next(uce_dtree*, uce_dtree_iter*, - const char** key, size_t* klen, uce_dtree** child); +uce_dv_iter uce_dv_iter_begin(uce_dvalue*); +int uce_dv_iter_next(uce_dvalue*, uce_dv_iter*, + const char** key, size_t* klen, uce_dvalue** child); /* encode/decode at the membrane */ -size_t uce_dtree_encode(uce_dtree*, char* buf, size_t cap); // → UCEB1 -uce_dtree* uce_dtree_decode(const char* buf, size_t len); +size_t uce_dv_encode(uce_dvalue*, char* buf, size_t cap); // → UCEB1 +uce_dvalue* uce_dv_decode(const char* buf, size_t len); /* ob / print / helpers: uce_print, uce_ob_start, uce_ob_get_close, uce_html_escape, uce_json_encode, ... (mirror uce_lib surface) */ ``` @@ -288,7 +291,7 @@ surfaced as error returns where fallible. ### 5.3 Wire encoding "UCEB1" (membrane + cross-instance only) -Length-prefixed binary tree; **not** JSON. Sketch (finalize against DTree's +Length-prefixed binary tree; **not** JSON. Sketch (finalize against DValue's actual fields — value + ordered children): ``` @@ -302,6 +305,12 @@ header := "UCEB" u8 version This encoding is a **versioned protocol** from day one (header byte). It is the Plane B contract and the membrane format; internal calls never see it. +UCEB1 encoding/decoding should also be exposed to the unit developer so +they can make use of fast serialization/deserialization: matching our existing +API conventions these should be ucb_encode(DValue val) and ucb_decode(String val). This may also be +a worthwhile target for session variables storage (either change session +to DValue or add StringMap support to UCEB1 ser/de). + ### 5.4 Unit module contract ``` @@ -309,7 +318,7 @@ custom sections: dylink.0 (standard), uce.abi { abi_version, toolchain_id } imports: env.memory, env.__indirect_function_table, env.__memory_base, env.__table_base, GOT.mem.* / GOT.func.* (resolved by loader), - core symbols (malloc, uce_dtree_*, uce_print, ...) + core symbols (malloc, uce_dv_*, uce_print, ...) exports: uce_unit_setup, uce_unit_render, uce_unit_component, uce_unit_websocket (same roles as today's UCE_SETUP/RENDER/COMPONENT/WEBSOCKET @@ -358,9 +367,9 @@ the Memory object, so OS-level CoW is available). No units are pre-fed. ## 7. Request lifecycle (replaces the native flow in linux_fastcgi.cpp) ``` -1. accept request +1. accept request (fastcgi, websockets message, socket event, CLI request) 2. workspace = birth_from_core_snapshot() (CoW, ~µs) -3. write wire-encoded context into workspace; core decodes → context DTree +3. write wire-encoded context into workspace; core decodes → context DValue 4. resolve entry unit (load on first call); call uce_unit_render(ctx_ptr) 5. component(path) inside guest → resolve hostcall → (lazy load) → call_indirect — reference semantics throughout @@ -387,30 +396,29 @@ Memory limit: linear memory max → allocation failure / trap → same path. - The site tree, docs, demo, and the network test suite (`tests/run_network_tests.py`) — which becomes the parity harness (§9). - nginx/FastCGI front-end integration, worker model, websocket event flow - (events re-enter via the websocket entry point; note: a workspace-per-event - or workspace-per-connection decision is an open question, §11). + (events re-enter via the websocket entry point). --- ## 9. Implementation plan Phases are sequential; each has an exit criterion. No phase except 0 and 2's -scaffolding produces throwaway work. +scaffolding produces throwaway work. All dependencies must be vendored. -**Phase 0 — toolchain & runtime spike (timeboxed).** +**Phase 0 — toolchain & runtime.** Validate: wasi-sdk `-fPIC` + `wasm-ld -shared` on a representative generated unit; cross-module C++ calls with shared memory/table; exceptions decision (wasm EH vs. error-code discipline at unit boundaries — pick one, record it); vendored runtime selection. Candidates: **WAMR** (C, small, designed for embedding, easiest to vendor and patch — fits the project's vendoring -practice) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier -to vendor/patch). Selection criteria: imported-memory + shared-table support, +practice and would be preferred) vs. **Wasmtime** (fastest, best AOT/CoW machinery, Rust — heavier +to vendor/patch, use only if blocked on WAMR). Selection criteria: imported-memory + shared-table support, AOT artifact quality, patchability. Exit: a two-module (core stub + unit stub) hello-world linked at runtime by a minimal loader, in the chosen vendored runtime. -**Phase 1 — DTree C ABI in the native runtime.** -Introduce `uce_dtree_*` (§5.2) and the UCEB1 codec in `src/lib/`, used +**Phase 1 — DValue C ABI in the native runtime.** +Introduce `uce_dv_*` (§5.2) and the UCEB1 codec in `src/lib/`, used natively. Zero wasm dependency; immediately testable; freezes the contract everything else builds on. Exit: codec round-trip + accessor tests in the existing suite; ABI doc checked in. @@ -443,16 +451,15 @@ on the site tree; audit `site/` for cross-request-static reliance (§3.2 breaking change); benchmark suite (template-heavy page, sqlite page, component-heavy starter page) with budgets: ≤2× native page latency, workspace birth ≤100µs, internal component call overhead within 10× native -call cost. Exit: numbers published in this document; go/no-go for default -backend. +call cost. Exit: numbers published in this document, all tests and reviews pass. **Phase 6 — second plane (deferred until wanted).** Cross-instance call mechanism (props-in/output-out, UCEB1), first Plane B language binding, loader enforcement of the cross-plane context rule (§4). -Plane A second language (Rust) as the cheaper first polyglot proof. -The native `.so` backend remains in-tree and selectable until Phase 5's -go/no-go; both backends share the Phase 1 C ABI. +The native `.so` backend remains in-tree (as a reference) and selectable by config +but we switch over to the wasm backend as soon as it's available and test +only on that; both backends share the Phase 1 C ABI. --- @@ -468,18 +475,22 @@ go/no-go; both backends share the Phase 1 C ABI. | Multi-module DWARF / debugging story | Trap stack traces cover the production case (better than today); accept weaker interactive debugging; keep native backend for local deep-debugging | | Unit-statics semantic change breaks existing pages | Phase 5 audit of `site/`; documented migration note; host-side cache facility if a real need surfaces | -## 11. Open questions +## 11. Decisions -1. **Exceptions:** wasm EH or error codes at unit boundaries? (Phase 0.) +1. **Exceptions:** wasm EH or error codes at unit boundaries? Error codes. 2. **WebSocket granularity:** workspace per event (pure arena, statics reset per event) or per connection (state across events, bounded lifetime)? - Leaning per-event for consistency; needs a look at current WS page usage. -3. **UCEB1 final layout** vs. DTree's actual field set (value/children/list - flag) — finalize in Phase 1. + Per-event. Should be compatible with WS since the WS contract is: runtime holds + and brokers connections, makes request to unit's WS() {...} directive, unit + may decide to send data back over WS to any or even all connected clients. +3. **UCEB1 final layout** vs. DValue's actual field set (value/children/list + flag) — somewhat open, finalize in Phase 1. 4. **Cursor vs. bulk** as the default for `sqlite_query` results at the - membrane (offer both; pick the default after Phase 5 benchmarks). + membrane (offer both; pick the default after Phase 5 benchmarks, tending towards cursor right now). 5. **Streaming output:** today's ob model buffers; does the membrane expose - `uce_host_stream_write` from day one or post-MVP? + `uce_host_stream_write` from day one or post-MVP? Undecided. ob is a critical + abstraction to our whole execution model and whether we expose direct stream + write or not, the existing PHP-like ob contract/paradigm must stay in place. 6. **Core snapshot rebuild cadence** once placement memoization lands (dead-base reclamation policy). @@ -488,7 +499,7 @@ go/no-go; both backends share the Phase 1 C ABI. The module is the **unit** (file-grained, lazily compiled, lazily loaded — including mid-request). The instance set is the **workspace** (one per request; shared memory + table; born from a core-only CoW snapshot; dropped -wholesale — the arena, done right). The contract is the **DTree C ABI** +wholesale — the arena, done right). The contract is the **DValue C ABI** inside the workspace (pointer semantics, no serialization) and the **UCEB1 wire encoding + handles** at every true address-space boundary (host membrane, Plane B languages, future trust boundaries). Serialization diff --git a/docs/react-developer-affordances-todo.md b/docs/react-developer-affordances-todo.md index e154a1a..81d8b04 100644 --- a/docs/react-developer-affordances-todo.md +++ b/docs/react-developer-affordances-todo.md @@ -28,7 +28,7 @@ Legend: `[ ]` not started, `[~]` in progress, `[x]` done, `[!]` blocked, `[-]` s - Why: React-framework developers routinely shape arrays/objects near render code. - Done when: helpers are declared, implemented, documented, and covered by tests. - Verify: build plus focused site/network tests. - - [x] G1.1: Identify current `StringList`/`DTree` idioms and choose helper surface. + - [x] G1.1: Identify current `StringList`/`DValue` idioms and choose helper surface. - [x] G1.2: Implement minimal high-value helpers without broad template complexity. - [x] G1.3: Add docs and examples for helpers. - [x] G1.4: Add/extend tests. @@ -67,7 +67,7 @@ Complete. - 2026-05-28: Defer component children/slots and JSX-like preprocessor syntax. - 2026-05-28: Do not add global runtime head/assets/islands APIs; implement asset/island as starter-local components. - 2026-05-28: Do not add a generic runtime file-based-routing system; demonstrate hierarchical/file routing inside starter `index.uce`. -- 2026-05-28: Keep collection helpers explicit (`list_*`, `dtree_*`) instead of overloading generic names such as `map`/`sort`. +- 2026-05-28: Keep collection helpers explicit (`list_*`, `dv_*`) instead of overloading generic names such as `map`/`sort`. ## Assumptions @@ -85,7 +85,7 @@ Complete. - 2026-05-28: `ssh k-uce 'cd /Code/uce.openfu.com/uce && bash scripts/build_linux.sh'` succeeded. - 2026-05-28: Restarted `uce.service` on `k-uce`. - 2026-05-28: `tests/run_network_tests.py --match core` passed. -- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dtree_group_by`. +- 2026-05-28: Manual checks returned `200` for `/examples/uce-starter/index.uce`, `?dashboard`, `?workspace/projects`, `?themes`, `/demo/collections.uce`, `/doc/index.uce?p=coming_from_react`, `/doc/index.uce?p=list_map`, and `/doc/index.uce?p=dv_group_by`. - 2026-05-28: Full internal network suite passed, 25/25. - 2026-05-28: Temporary broken `/tests/diagnostic-probe.uce` returned `500` with formatted `UCE compile error` diagnostics; source and cache artifacts were removed afterward. diff --git a/scripts/refactor/rename-dvalue-api.py b/scripts/refactor/rename-dvalue-api.py new file mode 100755 index 0000000..93e4b42 --- /dev/null +++ b/scripts/refactor/rename-dvalue-api.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Mechanical one-shot rename script for UCE's legacy value-tree API. + +The old tokens are constructed rather than written literally so this script can +remain in the tree after the rename without reintroducing the retired names. +""" +from __future__ import annotations + +import os +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +OLD_TYPE = "D" + "Tree" +OLD_LOWER = "d" + "tree" +NEW_TYPE = "DValue" +NEW_LOWER = "dvalue" + +DIR_EXCLUDES = { + ".git", + ".hg", + ".svn", + "3rdparty", + "bin", + "node_modules", + "pkg", + "tmp", + "work", + "__pycache__", +} + +BINARY_SUFFIXES = { + ".bin", + ".gif", + ".ico", + ".jpg", + ".jpeg", + ".lock", + ".o", + ".pdf", + ".png", + ".pyc", + ".so", + ".sqlite", + ".webp", + ".zip", +} + +REPLACEMENTS = ( + (OLD_TYPE, NEW_TYPE), + (OLD_TYPE.upper(), NEW_TYPE.upper()), + (OLD_LOWER + "_", "dv_"), + (OLD_LOWER, NEW_LOWER), +) + + +def replace_text(value: str) -> str: + for old, new in REPLACEMENTS: + value = value.replace(old, new) + return value + + +def skip_dir(name: str) -> bool: + return name in DIR_EXCLUDES or name.startswith(".fuse") + + +def skip_file(path: Path) -> bool: + return path.name.startswith(".fuse") or path.suffix in BINARY_SUFFIXES + + +def rewrite_text_files() -> int: + changed = 0 + for current, dirs, files in os.walk(ROOT): + dirs[:] = [name for name in dirs if not skip_dir(name)] + for name in files: + path = Path(current) / name + if skip_file(path): + continue + data = path.read_bytes() + if b"\0" in data: + continue + try: + original = data.decode("utf-8") + except UnicodeDecodeError: + continue + updated = replace_text(original) + if updated != original: + path.write_text(updated, encoding="utf-8") + changed += 1 + return changed + + +def rename_paths() -> int: + changed = 0 + matches: list[Path] = [] + for current, dirs, files in os.walk(ROOT, topdown=False): + dirs[:] = [name for name in dirs if not skip_dir(name)] + current_path = Path(current) + for name in files + dirs: + new_name = replace_text(name) + if new_name != name: + matches.append(current_path / name) + for path in sorted(matches, key=lambda item: len(item.parts), reverse=True): + if not path.exists(): + continue + target = path.with_name(replace_text(path.name)) + if target.exists(): + raise RuntimeError(f"cannot rename {path} -> {target}: target exists") + path.rename(target) + changed += 1 + return changed + + +def main() -> None: + print(f"Updated {rewrite_text_files()} files and renamed {rename_paths()} paths.") + + +if __name__ == "__main__": + main() diff --git a/site/demo/call_file_funcs.uce b/site/demo/call_file_funcs.uce index 84026d7..27fcaa9 100644 --- a/site/demo/call_file_funcs.uce +++ b/site/demo/call_file_funcs.uce @@ -1,6 +1,6 @@ // Minimal exported helper used by the `unit_call()` demo page. -EXPORT DTree* test_func(DTree* call_param) +EXPORT DValue* test_func(DValue* call_param) { print("HELLO FROM TEST FUNCTION"); return(0); diff --git a/site/demo/collections.uce b/site/demo/collections.uce index 27c4adc..36d237e 100644 --- a/site/demo/collections.uce +++ b/site/demo/collections.uce @@ -2,7 +2,7 @@ RENDER(Request& context) { - DTree p; + DValue p; p.set(context.params); StringList routes = {"index", "dashboard", "themes", "dashboard", "workspace/projects"}; @@ -10,15 +10,15 @@ RENDER(Request& context) StringList sorted_routes = list_sort(unique_routes); StringList labels = map(sorted_routes, [](String route) { return(to_upper(replace(route, "/", " / "))); }); - DTree cards; - DTree card; + DValue cards; + DValue card; card["title"] = "Dashboard"; card["section"] = "app"; card["href"] = "?dashboard"; cards.push(card); card.clear(); card["title"] = "Themes"; card["section"] = "app"; card["href"] = "?themes"; cards.push(card); card.clear(); card["title"] = "Docs"; card["section"] = "reference"; card["href"] = "../doc/index.uce"; cards.push(card); - DTree app_cards = dtree_filter(cards, [](DTree item, String key) { return(item["section"].to_string() == "app"); }); - DTree titles = dtree_map(app_cards, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); }); - DTree grouped = dtree_group_by(cards, [](DTree item, String key) { return(item["section"].to_string()); }); + 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()); }); <> @@ -30,7 +30,7 @@ RENDER(Request& context)

Collection Helpers

Small data-shaping helpers are useful for route lists, nav records, cards, and other render-adjacent structures.

StringList route labels

-

DTree app card titles

+

DValue app card titles

Grouped cards

Request Parameters diff --git a/site/demo/components.uce b/site/demo/components.uce index fd60e42..cff33a1 100644 --- a/site/demo/components.uce +++ b/site/demo/components.uce @@ -1,11 +1,11 @@ RENDER(Request& context) { - DTree card_props; + DValue card_props; card_props["title"] = "Component Example"; card_props["body"] = "This card body comes from context.props and is rendered through component()."; - DTree named_props; + DValue named_props; named_props["title"] = "Named Render Example"; named_props["body"] = "This content is rendered through the COMPONENT:BODY(Request& context) entry point."; diff --git a/site/demo/demo_guard.h b/site/demo/demo_guard.h index 1bce35b..13eb4f4 100644 --- a/site/demo/demo_guard.h +++ b/site/demo/demo_guard.h @@ -78,7 +78,7 @@ void test_demo_render_restricted_json(Request& context, String title, String ris { context.set_status(403, "Restricted"); context.header["Content-Type"] = "application/json; charset=utf-8"; - DTree payload; + DValue payload; payload["ok"].set_bool(false); payload["error"] = "restricted"; payload["title"] = title; diff --git a/site/demo/dtree.uce b/site/demo/dvalue.uce similarity index 86% rename from site/demo/dtree.uce rename to site/demo/dvalue.uce index 1be7961..bc499c3 100644 --- a/site/demo/dtree.uce +++ b/site/demo/dvalue.uce @@ -2,13 +2,13 @@ RENDER(Request& context) { - DTree t; + DValue t; <>

UCE Test: - DTree + DValue

Value Types

@@ -33,21 +33,21 @@ RENDER(Request& context)
 f64: ", scalar_text.to_f64(), "\n");
 		print("text -> u64: ", scalar_text.to_u64(), "\n");
 		print("text -> s64: ", scalar_text.to_s64(), "\n");
 
-		DTree bool_text;
+		DValue bool_text;
 		bool_text = "yes";
 		print("yes -> bool: ", bool_text.to_bool() ? "true" : "false", "\n");
 
-		DTree single_value_map;
+		DValue single_value_map;
 		single_value_map["value"] = "123";
 		print("{value:123} -> u64: ", single_value_map.to_u64(), "\n");
 
-		DTree headers;
+		DValue headers;
 		headers["Content-Type"] = "text/plain";
 		headers["X-Test"] = "123";
 		print("map -> StringMap:\n", var_dump(headers.to_stringmap()), "\n");
@@ -58,12 +58,12 @@ RENDER(Request& context)
 
 		
set("Ada");
 		print("has(user) after create: ", lookup.has("user") ? "true" : "false", "\n");
-		if(DTree* user = lookup.key("user"))
+		if(DValue* user = lookup.key("user"))
 			print("key(user) after create: ", user->to_string(), "\n");
 
 		?>
@@ -90,11 +90,11 @@ RENDER(Request& context)

 		
diff --git a/site/demo/fileio.uce b/site/demo/fileio.uce
index b7b2c30..2bdd2a5 100644
--- a/site/demo/fileio.uce
+++ b/site/demo/fileio.uce
@@ -2,7 +2,7 @@
 
 RENDER(Request& context)
 {
-	DTree t;
+	DValue t;
 
 	<>
 		
diff --git a/site/demo/header.uce b/site/demo/header.uce
index 3071d1c..be0c802 100644
--- a/site/demo/header.uce
+++ b/site/demo/header.uce
@@ -2,7 +2,7 @@
 
 RENDER(Request& context)
 {
-	DTree t;
+	DValue t;
 
 	<>
 		
diff --git a/site/demo/index.uce b/site/demo/index.uce
index 211934d..3a9fa4c 100644
--- a/site/demo/index.uce
+++ b/site/demo/index.uce
@@ -12,7 +12,7 @@ void render_card(String url, String title, String desc)
 
 RENDER(Request& context)
 {
-	DTree p;
+	DValue p;
 	p.set(context.params);
 	bool allow_server_demos = test_demo_request_allowed(context);
 
@@ -35,10 +35,10 @@ RENDER(Request& context)
 			
 
 			
Data Types & Parsing
- + - - + + diff --git a/site/demo/json.uce b/site/demo/json.uce index 56ddf6f..726e941 100644 --- a/site/demo/json.uce +++ b/site/demo/json.uce @@ -2,13 +2,13 @@ RENDER(Request& context) { - DTree t; + DValue t; <>

UCE Test: - DTree/JSON + DValue/JSON

JSON: diff --git a/site/demo/markdown.uce b/site/demo/markdown.uce index 69f8337..07966d9 100644 --- a/site/demo/markdown.uce +++ b/site/demo/markdown.uce @@ -1,6 +1,6 @@ RENDER(Request& context) { - DTree options; + DValue options; options["components"][":::warning"] = "components/markdown/warning"; options["components"]["node.code_block"] = "components/markdown/code_block"; @@ -9,7 +9,7 @@ RENDER(Request& context) file_get_contents("markdown-example.md") ); - DTree ast = markdown_to_ast(markdown_src, options); + DValue ast = markdown_to_ast(markdown_src, options); String html = markdown_to_html(markdown_src, options); <> diff --git a/site/demo/memcached.uce b/site/demo/memcached.uce index 1aac709..b70903a 100644 --- a/site/demo/memcached.uce +++ b/site/demo/memcached.uce @@ -9,7 +9,7 @@ RENDER(Request& context) test_demo_render_restricted_html(context, "MemcacheD", "inspect and mutate local memcached state"); return; } - DTree t; + DValue t; <> diff --git a/site/demo/once-init.uce b/site/demo/once-init.uce index d8d86da..f9f9aee 100644 --- a/site/demo/once-init.uce +++ b/site/demo/once-init.uce @@ -28,13 +28,13 @@ COMPONENT:PROBE(Request& context) RENDER(Request& context) { - DTree first; + DValue first; first["label"] = "First component call"; - DTree second; + DValue second; second["label"] = "Second component call in the same request"; - DTree third; + DValue third; third["label"] = "Third component call through unit_call(\"COMPONENT:PROBE\")"; <> diff --git a/site/demo/regex.uce b/site/demo/regex.uce index c6c44a7..73766e9 100644 --- a/site/demo/regex.uce +++ b/site/demo/regex.uce @@ -19,8 +19,8 @@ void regex_bool_row(String label, bool result, bool expect) RENDER(Request& context) { String text = "Contact ops@example.test or tag #uce, #docs, and café."; - DTree first_email = regex_search("(?[A-Za-z0-9._%+-]+)@(?[A-Za-z0-9.-]+)", text); - DTree tags = regex_search_all("#(?[A-Za-z0-9_]+)", text); + DValue first_email = regex_search("(?[A-Za-z0-9._%+-]+)@(?[A-Za-z0-9.-]+)", text); + DValue tags = regex_search_all("#(?[A-Za-z0-9_]+)", text); StringList pieces = regex_split("\\s*,\\s*", "uce, components, markdown"); <> @@ -38,7 +38,7 @@ RENDER(Request& context) Regular Expressions -

UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DTree values.

+

UCE regex functions use PCRE2 and return ordinary UCE strings, lists, and DValue values.

sample =

Validation And Search

diff --git a/site/demo/sqlite.uce b/site/demo/sqlite.uce index f05d9de..d519038 100644 --- a/site/demo/sqlite.uce +++ b/site/demo/sqlite.uce @@ -23,7 +23,7 @@ RENDER(Request& context) return; } - DTree notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10"); + DValue notes = sqlite_query(db, "select id, body, created_at from notes order by id desc limit 10"); String error = sqlite_error(db); ?> @@ -41,7 +41,7 @@ RENDER(Request& context)
- +
# diff --git a/site/demo/task.uce b/site/demo/task.uce index 631c27b..9face15 100644 --- a/site/demo/task.uce +++ b/site/demo/task.uce @@ -8,7 +8,7 @@ RENDER(Request& context) test_demo_render_restricted_html(context, "Tasks", "spawn and inspect background tasks"); return; } - DTree t; + DValue t; String task_name = first(context.get["task-name"], "example-task"); diff --git a/site/demo/task_repeat.uce b/site/demo/task_repeat.uce index 5dfefab..46282a1 100644 --- a/site/demo/task_repeat.uce +++ b/site/demo/task_repeat.uce @@ -8,7 +8,7 @@ RENDER(Request& context) test_demo_render_restricted_html(context, "Task repeat", "schedule recurring background tasks"); return; } - DTree t; + DValue t; String task_name = first(context.get["task-name"], "example-task"); diff --git a/site/demo/unit-browser.uce b/site/demo/unit-browser.uce index 5320e3c..9641ee4 100644 --- a/site/demo/unit-browser.uce +++ b/site/demo/unit-browser.uce @@ -27,7 +27,7 @@ String unit_status_class(String compile_status, String error_status) return("status-muted"); } -String unit_flag_label(DTree value) +String unit_flag_label(DValue value) { return(value.to_string() == "(true)" ? "✅" : "❌"); } @@ -61,7 +61,7 @@ RENDER(Request& context) if(selected_path == "" && unit_paths.size() > 0) selected_path = unit_paths.front(); - DTree selected_info = unit_info(selected_path); + DValue selected_info = unit_info(selected_path); String selected_compile_status = selected_info["compile_status"].to_string(); String selected_error_status = selected_info["error_status"].to_string(); String selected_status_class = unit_status_class(selected_compile_status, selected_error_status); diff --git a/site/demo/unit-info.uce b/site/demo/unit-info.uce index 97c9f95..ed77077 100644 --- a/site/demo/unit-info.uce +++ b/site/demo/unit-info.uce @@ -3,7 +3,7 @@ RENDER(Request& context) { bool allow_mutation = test_demo_request_allowed(context); - DTree payload; + DValue payload; if(context.get["compile"] != "") { @@ -19,11 +19,11 @@ RENDER(Request& context) payload["current"] = unit_info(); - DTree units; + DValue units; units.set_array(); for(auto& unit_path : units_list()) { - DTree item; + DValue item; item = unit_path; units.push(item); } diff --git a/site/demo/websockets.ws.uce b/site/demo/websockets.ws.uce index de6dd46..6cf4940 100644 --- a/site/demo/websockets.ws.uce +++ b/site/demo/websockets.ws.uce @@ -8,9 +8,9 @@ String clean_chat_field(String raw, u32 max_length) return(value); } -DTree chat_event(Request& context, String type, String body) +DValue chat_event(Request& context, String type, String body) { - DTree event; + DValue event; event["type"] = type; event["body"] = body; event["connection_id"] = context.params["WS_CONNECTION_ID"]; @@ -159,7 +159,7 @@ WS(Request& context) return; } - DTree payload = json_decode(context.in); + DValue payload = json_decode(context.in); String type = clean_chat_field(payload["type"].to_string(), 24); String name = clean_chat_field(payload["name"].to_string(), 32); String body = clean_chat_field(payload["body"].to_string(), 500); @@ -183,7 +183,7 @@ WS(Request& context) context.connection["last_type"] = type; context.connection["last_body"] = body; context.connection["message_count"] = (f64)(message_count + 1); - DTree event = chat_event(context, "message", body); + DValue event = chat_event(context, "message", body); ws_send(json_encode(event)); return; } diff --git a/site/demo/xml.uce b/site/demo/xml.uce index 6fcd610..43134c9 100644 --- a/site/demo/xml.uce +++ b/site/demo/xml.uce @@ -2,31 +2,31 @@ RENDER(Request& context) { - DTree book; + DValue book; book["name"] = "book"; book["attrs"]["id"] = "b1"; book["attrs"]["type"] = "reference"; - DTree title; + DValue title; title["name"] = "title"; title["text"] = "UCE & XML"; book["children"].push(title); - DTree chapter; + DValue chapter; chapter["name"] = "chapter"; chapter["attrs"]["number"] = "1"; chapter["text"] = "Structural conversion without schema validation."; book["children"].push(chapter); String encoded = xml_encode(book); - DTree decoded = xml_decode(encoded); + DValue decoded = xml_decode(encoded); - DTree simple; + DValue simple; simple["title"] = "Hello"; simple["count"] = "3"; String incoming = "UCEAB"; - DTree incoming_tree = xml_decode(incoming); + DValue incoming_tree = xml_decode(incoming); <> @@ -35,12 +35,12 @@ RENDER(Request& context) XML -

xml_encode() and xml_decode() convert between XML strings and element-shaped DTree values without schema validation.

+

xml_encode() and xml_decode() convert between XML strings and element-shaped DValue values without schema validation.

Encoded Element Tree

-

Decoded DTree

+

Decoded DValue

Simple Map Encoding

@@ -49,7 +49,7 @@ RENDER(Request& context)

Decode Existing XML

Input XML:

-

Decoded DTree:

+

Decoded DValue:

diff --git a/site/demo/yaml.uce b/site/demo/yaml.uce index 52dfac0..82de3ac 100644 --- a/site/demo/yaml.uce +++ b/site/demo/yaml.uce @@ -12,19 +12,19 @@ RENDER(Request& context) " - cache\n" "message: |\n" " Keep config files readable.\n" - " Load them as DTree values.\n"; + " Load them as DValue values.\n"; - DTree cfg = yaml_decode(source); + DValue cfg = yaml_decode(source); String encoded = yaml_encode(cfg); - DTree roundtrip = yaml_decode(encoded); + DValue roundtrip = yaml_decode(encoded); - DTree generated; + DValue generated; generated["database"]["host"] = "localhost"; generated["database"]["port"] = (f64)3306; generated["features"]["components"].set_bool(true); generated["features"]["markdown"].set_bool(true); - DTree theme; + DValue theme; theme = "clean"; generated["themes"].push(theme); theme = "compact"; @@ -37,12 +37,12 @@ RENDER(Request& context) YAML -

yaml_encode() and yaml_decode() convert concise config-style YAML to and from DTree values.

+

yaml_encode() and yaml_decode() convert concise config-style YAML to and from DValue values.

Config Source

-

Decoded DTree

+

Decoded DValue

Encoded Again

diff --git a/site/demo/zip.uce b/site/demo/zip.uce index 12476f1..7760201 100644 --- a/site/demo/zip.uce +++ b/site/demo/zip.uce @@ -14,12 +14,12 @@ RENDER(Request& context) mkdir(base); mkdir(extract_dir); - DTree entries; + DValue entries; entries["hello.txt"] = "Hello from a generated ZIP archive.\n"; entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n"; zip_create(archive, entries); - DTree listing = zip_list(archive); + DValue listing = zip_list(archive); String hello = zip_read(archive, "hello.txt"); zip_extract(archive, extract_dir); String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt")); diff --git a/site/doc/areas/types.txt b/site/doc/areas/types.txt index 2969d5a..1d75306 100644 --- a/site/doc/areas/types.txt +++ b/site/doc/areas/types.txt @@ -2,14 +2,14 @@ Types 0_Request array_merge -0_DTree -dtree_filter -dtree_group_by -dtree_keys -dtree_map -dtree_omit -dtree_pick -dtree_values +0_DValue +dv_filter +dv_group_by +dv_keys +dv_map +dv_omit +dv_pick +dv_values each get_by_path has diff --git a/site/doc/pages/0_DTree.txt b/site/doc/pages/0_DValue.txt similarity index 73% rename from site/doc/pages/0_DTree.txt rename to site/doc/pages/0_DValue.txt index 5ca8d3d..26f635a 100644 --- a/site/doc/pages/0_DTree.txt +++ b/site/doc/pages/0_DValue.txt @@ -1,8 +1,8 @@ :title -DTree +DValue :sig -DTree +DValue :see >types @@ -10,23 +10,23 @@ get_by_path json_decode :content -`DTree` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers. +`DValue` is UCE's general-purpose structured value type. It is the runtime's default container for nested data such as configuration trees, call payloads, decoded JSON, connection state, and metadata returned by runtime helpers. ## Value Kinds -`DTree` can hold: +`DValue` can hold: - `String` - `f64` - `bool` - pointer values -- nested child `DTree` values in a map-shaped container +- nested child `DValue` values in a map-shaped container -Map-shaped `DTree` values can also represent list-like data when their keys are numeric strings in sequence. +Map-shaped `DValue` values can also represent list-like data when their keys are numeric strings in sequence. ## Where It Appears -You will encounter `DTree` throughout the runtime, especially in: +You will encounter `DValue` throughout the runtime, especially in: - `context.cfg` - `context.props` @@ -47,7 +47,7 @@ You will encounter `DTree` 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 DTree&` 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 (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules: ```cpp String title = context.props["title"].to_string("Untitled"); @@ -56,7 +56,7 @@ 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. -`json_decode()` currently stores JSON numbers as string-valued `DTree` nodes, so typed numeric conversion is the normal way to consume those values. +`json_decode()` currently stores JSON numbers as string-valued `DValue` nodes, so typed numeric conversion is the normal way to consume those values. References are dereferenced automatically in most normal reads. @@ -100,9 +100,9 @@ Useful inspection helpers include: ## each() -`each(std::function f)` iterates over the current tree value (see the `each` page for details). +`each(std::function f)` iterates over the current tree value (see the `each` page for details). -For map-shaped `DTree` values, the callback runs once per child entry and receives: +For map-shaped `DValue` values, the callback runs once per child entry and receives: - `t` as the child value, by const reference (no copy) - `key` as the child key @@ -113,7 +113,7 @@ For non-map values, `each()` still invokes the callback once: - `key` is an empty string ```cpp -context.connection["items"].each([&](const DTree& item, String key) { +context.connection["items"].each([&](const DValue& item, String key) { print(key, ": ", item.to_string(), "\n"); }); ``` @@ -127,7 +127,7 @@ u64 compiled_mtime = unit_info("test/hello.uce")["compiled_mtime"].to_u64(); bool dark_mode = context.props["dark_mode"].to_bool(); -if(DTree* user = payload.key("user")) { +if(DValue* user = payload.key("user")) { print(user->to_json()); } diff --git a/site/doc/pages/0_Request.txt b/site/doc/pages/0_Request.txt index 65416ae..d283b3a 100644 --- a/site/doc/pages/0_Request.txt +++ b/site/doc/pages/0_Request.txt @@ -24,7 +24,7 @@ ws_message ws_connection_id ws_connections ws_send -0_DTree +0_DValue StringMap UploadedFile @@ -120,7 +120,7 @@ Raw request body. For WebSocket handlers, this is the current message payload. Use this for JSON APIs: ```cpp -DTree body = json_decode(context.in); +DValue body = json_decode(context.in); ``` ### `context.uploaded_files` @@ -165,7 +165,7 @@ Related helpers: ### `context.call` -Type: `DTree` +Type: `DValue` General request-local scratch/configuration tree. It is shared by the page, components, and unit calls participating in the current request. Use it for app-level request state, fragments, router results, page type, page title, and other values that need to be read by later components. @@ -181,7 +181,7 @@ Prefer clear top-level names when state is app-wide (`route`, `fragments`) and n ### `context.cfg` -Type: `DTree` +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: @@ -199,19 +199,19 @@ String site_name = context.cfg.get_by_path("site/name").to_string(); ### `context.props` -Type: `DTree` +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 -DTree props; +DValue props; props["title"] = "Dashboard"; print(component("components/card", props, context)); ``` ### `context.connection` -Type: `DTree` +Type: `DValue` WebSocket connection-local state. Mutations persist across `WS(Request& context)` calls for the same socket. @@ -394,7 +394,7 @@ RENDER(Request& context) RENDER(Request& context) { context.header["Content-Type"] = "application/json"; - DTree response; + DValue response; response["ok"].set_bool(true); print(json_encode(response)); } @@ -413,7 +413,7 @@ RENDER(Request& context) ### Component props ```cpp -DTree props; +DValue props; props["title"] = "Welcome"; print(component("components/card", props, context)); ``` diff --git a/site/doc/pages/1_CLI.txt b/site/doc/pages/1_CLI.txt index f88eabc..60054dd 100644 --- a/site/doc/pages/1_CLI.txt +++ b/site/doc/pages/1_CLI.txt @@ -46,12 +46,12 @@ Inside the handler, the usual `Request& context` fields are available: CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly. -Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`. +Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DValue`. ```cpp CLI(Request& context) { - DTree input = cli_input(context); + DValue input = cli_input(context); String action = first(input["action"].to_string(), "ping"); if(action == "ping") { diff --git a/site/doc/pages/1_WS.txt b/site/doc/pages/1_WS.txt index 5362c4d..e66891f 100644 --- a/site/doc/pages/1_WS.txt +++ b/site/doc/pages/1_WS.txt @@ -20,7 +20,7 @@ UCE reassembles fragmented messages before calling `WS(Request& context)`. Text ## Connection State -`context.connection` is a broker-owned `DTree` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection. +`context.connection` is a broker-owned `DValue` for the current socket. It starts empty for a new client and persists across later `WS(Request& context)` calls on that same connection. ## Message Data diff --git a/site/doc/pages/array_merge.txt b/site/doc/pages/array_merge.txt index 3d4a064..8091d54 100644 --- a/site/doc/pages/array_merge.txt +++ b/site/doc/pages/array_merge.txt @@ -1,6 +1,6 @@ :sig StringMap array_merge(StringMap a, StringMap b) -DTree array_merge(DTree a, DTree b) +DValue array_merge(DValue a, DValue b) :params a : left-hand source map or tree @@ -9,7 +9,7 @@ return value : merged result :see >types -0_DTree +0_DValue StringMap json_decode @@ -18,7 +18,7 @@ Merges two maps or trees using PHP-like merge behavior. For `StringMap`, keys from `b` overwrite keys from `a`. -For `DTree`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list. +For `DValue`, string keys from `b` overwrite keys from `a`. Numeric keys are appended and reindexed when either side behaves like a list. This helper is the closest UCE equivalent to PHP `array_merge()` for common request, config, and JSON-shaped data. diff --git a/site/doc/pages/cli_arg.txt b/site/doc/pages/cli_arg.txt index b042228..4b8e21c 100644 --- a/site/doc/pages/cli_arg.txt +++ b/site/doc/pages/cli_arg.txt @@ -21,4 +21,4 @@ CLI(Request& context) } ``` -For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly. +For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DValue` directly. diff --git a/site/doc/pages/cli_input.txt b/site/doc/pages/cli_input.txt index c2995d1..dff4571 100644 --- a/site/doc/pages/cli_input.txt +++ b/site/doc/pages/cli_input.txt @@ -2,17 +2,17 @@ cli_input :sig -DTree cli_input(Request& context) +DValue cli_input(Request& context) :see >1_CLI >json_decode ->DTree +>DValue :content Returns a structured parameter tree for a `CLI(Request& context)` invocation. -`cli_input()` merges simple command inputs into one `DTree`: +`cli_input()` merges simple command inputs into one `DValue`: 1. query parameters from `context.get` 2. form parameters from `context.post` @@ -25,7 +25,7 @@ If the JSON body is a scalar or array instead of an object, the decoded value is ```cpp CLI(Request& context) { - DTree input = cli_input(context); + DValue input = cli_input(context); String action = first(input["action"].to_string(), "help"); if(action == "echo") diff --git a/site/doc/pages/coming_from_react.txt b/site/doc/pages/coming_from_react.txt index 78c509e..8f6ae29 100644 --- a/site/doc/pages/coming_from_react.txt +++ b/site/doc/pages/coming_from_react.txt @@ -12,7 +12,7 @@ unit_render 3_C++ Preprocessor map filter -dtree_filter +dv_filter :content UCE is server-first C++ with a small template preprocessor. It does not try to be React, but several concepts map cleanly. @@ -47,8 +47,8 @@ The function library includes small collection helpers for common route/menu/car ```cpp auto visible = filter(routes, [](String route) { return(route != "admin"); }); auto labels = map(visible, [](String route) { return(to_upper(route)); }); -DTree app_items = dtree_filter(menu, [](DTree item, String key) { return(item["section"].to_string() == "app"); }); -DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); }); +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 the transformation communicates intent. Prefer explicit loops when side effects or multi-step validation are the main concern. diff --git a/site/doc/pages/component.txt b/site/doc/pages/component.txt index 541f7fe..05e3a3a 100644 --- a/site/doc/pages/component.txt +++ b/site/doc/pages/component.txt @@ -1,5 +1,5 @@ :sig -String component(String name, [DTree props], [Request& context]) +String component(String name, [DValue props], [Request& context]) :see >ob @@ -37,7 +37,7 @@ When a component unit defines `ONCE(Request& context)`, the runtime calls that h Default component handler: ```cpp -DTree props; +DValue props; props["title"] = "Status"; <> @@ -46,7 +46,7 @@ props["title"] = "Status"; Named component handler: ```cpp -DTree props; +DValue props; props["title"] = "System"; props["body"] = "Healthy"; @@ -76,7 +76,7 @@ COMPONENT:BODY(Request& context) Preparing props in C++ before rendering: ```cpp -DTree props; +DValue props; props["items"][0] = "alpha"; props["items"][1] = "beta"; props["items"][2] = "gamma"; diff --git a/site/doc/pages/component_render.txt b/site/doc/pages/component_render.txt index 20d5908..ea0d5fb 100644 --- a/site/doc/pages/component_render.txt +++ b/site/doc/pages/component_render.txt @@ -1,5 +1,5 @@ :sig -void component_render(String name, [DTree props], [Request& context]) +void component_render(String name, [DValue props], [Request& context]) :see >ob @@ -24,7 +24,7 @@ Use `component_render()` when you want to write component output directly from C ## Example ```cpp -DTree props; +DValue props; props["body"] = "Hello"; component_render("components/card:BODY", props, context); diff --git a/site/doc/pages/dtree_filter.txt b/site/doc/pages/dv_filter.txt similarity index 81% rename from site/doc/pages/dtree_filter.txt rename to site/doc/pages/dv_filter.txt index 7cedab5..97f41e5 100644 --- a/site/doc/pages/dtree_filter.txt +++ b/site/doc/pages/dv_filter.txt @@ -1,12 +1,12 @@ :title -dtree_filter +dv_filter :sig -DTree dtree_filter(DTree tree, function f) +DValue dv_filter(DValue tree, function f) :see StringList -0_DTree +0_DValue filter :content @@ -15,7 +15,7 @@ Keeps children for which f returns true. List-like input stays list-like. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree visible = dtree_filter(items, [](DTree item, String key) { return(item["hidden"].to_bool() == false); }); +DValue visible = dv_filter(items, [](DValue item, String key) { return(item["hidden"].to_bool() == false); }); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_group_by.txt b/site/doc/pages/dv_group_by.txt similarity index 81% rename from site/doc/pages/dtree_group_by.txt rename to site/doc/pages/dv_group_by.txt index 310c44d..4d99686 100644 --- a/site/doc/pages/dtree_group_by.txt +++ b/site/doc/pages/dv_group_by.txt @@ -1,12 +1,12 @@ :title -dtree_group_by +dv_group_by :sig -DTree dtree_group_by(DTree tree, function f) +DValue dv_group_by(DValue tree, function f) :see StringList -0_DTree +0_DValue filter :content @@ -15,7 +15,7 @@ Groups children into list-like buckets by the string returned from f. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree by_section = dtree_group_by(menu, [](DTree item, String key) { return(item["section"].to_string()); }); +DValue by_section = dv_group_by(menu, [](DValue item, String key) { return(item["section"].to_string()); }); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_pick.txt b/site/doc/pages/dv_keys.txt similarity index 82% rename from site/doc/pages/dtree_pick.txt rename to site/doc/pages/dv_keys.txt index e4b530b..643f60e 100644 --- a/site/doc/pages/dtree_pick.txt +++ b/site/doc/pages/dv_keys.txt @@ -1,21 +1,21 @@ :title -dtree_pick +dv_keys :sig -DTree dtree_pick(DTree tree, StringList keys) +StringList dv_keys(DValue tree) :see StringList -0_DTree +0_DValue filter :content -Copies only selected keys from a DTree map. +Returns map keys from a DValue. Scalar values produce an empty list. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree public_user = dtree_pick(user, {"name", "avatar"}); +StringList keys = dv_keys(context.cfg["menu"]); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_map.txt b/site/doc/pages/dv_map.txt similarity index 81% rename from site/doc/pages/dtree_map.txt rename to site/doc/pages/dv_map.txt index 597fc5d..7f74aee 100644 --- a/site/doc/pages/dtree_map.txt +++ b/site/doc/pages/dv_map.txt @@ -1,12 +1,12 @@ :title -dtree_map +dv_map :sig -DTree dtree_map(DTree tree, function f) +DValue dv_map(DValue tree, function f) :see StringList -0_DTree +0_DValue filter :content @@ -15,7 +15,7 @@ Transforms each child. List-like input stays list-like; map input keeps keys. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree titles = dtree_map(items, [](DTree item, String key) { DTree out; out = item["title"].to_string(); return(out); }); +DValue titles = dv_map(items, [](DValue item, String key) { DValue out; out = item["title"].to_string(); return(out); }); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_omit.txt b/site/doc/pages/dv_omit.txt similarity index 82% rename from site/doc/pages/dtree_omit.txt rename to site/doc/pages/dv_omit.txt index 91a1f30..9c64ae3 100644 --- a/site/doc/pages/dtree_omit.txt +++ b/site/doc/pages/dv_omit.txt @@ -1,21 +1,21 @@ :title -dtree_omit +dv_omit :sig -DTree dtree_omit(DTree tree, StringList keys) +DValue dv_omit(DValue tree, StringList keys) :see StringList -0_DTree +0_DValue filter :content -Copies a DTree map except for selected keys. +Copies a DValue map except for selected keys. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree safe_user = dtree_omit(user, {"password_hash"}); +DValue safe_user = dv_omit(user, {"password_hash"}); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_keys.txt b/site/doc/pages/dv_pick.txt similarity index 82% rename from site/doc/pages/dtree_keys.txt rename to site/doc/pages/dv_pick.txt index 9175a83..1dea1c1 100644 --- a/site/doc/pages/dtree_keys.txt +++ b/site/doc/pages/dv_pick.txt @@ -1,21 +1,21 @@ :title -dtree_keys +dv_pick :sig -StringList dtree_keys(DTree tree) +DValue dv_pick(DValue tree, StringList keys) :see StringList -0_DTree +0_DValue filter :content -Returns map keys from a DTree. Scalar values produce an empty list. +Copies only selected keys from a DValue map. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -StringList keys = dtree_keys(context.cfg["menu"]); +DValue public_user = dv_pick(user, {"name", "avatar"}); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/dtree_values.txt b/site/doc/pages/dv_values.txt similarity index 84% rename from site/doc/pages/dtree_values.txt rename to site/doc/pages/dv_values.txt index b8dcd91..838157e 100644 --- a/site/doc/pages/dtree_values.txt +++ b/site/doc/pages/dv_values.txt @@ -1,21 +1,21 @@ :title -dtree_values +dv_values :sig -DTree dtree_values(DTree tree) +DValue dv_values(DValue tree) :see StringList -0_DTree +0_DValue filter :content -Returns child values as a list-like DTree. +Returns child values as a list-like DValue. These helpers are intentionally small data-shaping conveniences for render code, routers, and configuration trees. They are useful when porting habits from React/Next/Remix code where lists of routes, navigation items, cards, or records are transformed close to the rendering boundary. ```cpp -DTree menu_items = dtree_values(context.cfg["menu"]); +DValue menu_items = dv_values(context.cfg["menu"]); ``` Prefer these helpers over open-coded loops when the transformation itself is the important part of the code. Use an explicit loop when mutation, error handling, or side effects are the main concern. diff --git a/site/doc/pages/each.txt b/site/doc/pages/each.txt index 8201835..b35bb5b 100644 --- a/site/doc/pages/each.txt +++ b/site/doc/pages/each.txt @@ -1,5 +1,5 @@ :sig -void DTree::each(function f) const +void DValue::each(function f) const :params f : callback invoked per child with the child node and its key @@ -7,26 +7,26 @@ return value : none :see >types -0_DTree -dtree_map -dtree_filter -dtree_keys +0_DValue +dv_map +dv_filter +dv_keys is_list :content -Iterates a `DTree`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Iterates a `DValue`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. For map-shaped nodes the callback runs once per child, in key order, receiving the child and its key. For scalar nodes it runs exactly once with the node itself and an empty key. -The callback receives the child as `const DTree&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly: +The callback receives the child as `const DValue&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly: ```cpp -rows.each([&](const DTree& row, String key) { +rows.each([&](const DValue& row, String key) { out("
  • ", html_escape(row.get_by_path("title").to_string("Untitled")), "
  • "); }); ``` -Declaring the callback parameter as plain `DTree` also works but deep-copies every child; prefer `const DTree&`. To transform or filter into a new tree, use `dtree_map()` / `dtree_filter()` instead of mutating inside the callback. +Declaring the callback parameter as plain `DValue` also works but deep-copies every child; prefer `const DValue&`. To transform or filter into a new tree, use `dv_map()` / `dv_filter()` instead of mutating inside the callback. List-shaped trees (see `is_list`) iterate in numeric index order, matching `json_encode()` and the other serializers. Keyed maps iterate in string key order. diff --git a/site/doc/pages/error_pages.txt b/site/doc/pages/error_pages.txt index ee49da6..a46974c 100644 --- a/site/doc/pages/error_pages.txt +++ b/site/doc/pages/error_pages.txt @@ -14,7 +14,7 @@ page_runtime_error : UCE page rendered (status 500) after a recovered fault or u :see >runtime 0_Request -0_DTree +0_DValue set_status :content diff --git a/site/doc/pages/get_by_path.txt b/site/doc/pages/get_by_path.txt index 7d3eb44..f8821d4 100644 --- a/site/doc/pages/get_by_path.txt +++ b/site/doc/pages/get_by_path.txt @@ -1,13 +1,13 @@ :sig -DTree DTree::get_by_path(String path, String delim = "/") const +DValue DValue::get_by_path(String path, String delim = "/") const :params path : slash-delimited path to traverse delim : optional path separator -return value : the resolved child node, or an empty `DTree` when the path cannot be followed +return value : the resolved child node, or an empty `DValue` when the path cannot be followed :see -0_DTree +0_DValue 0_Request >types json_decode @@ -15,9 +15,9 @@ has to_string :content -Traverses a nested `DTree` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries. +Traverses a nested `DValue` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries. -Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DTree`. +Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DValue`. A missing path therefore reads like an empty value — combine it with the `to_*` default arguments to express a fallback in one call: diff --git a/site/doc/pages/has.txt b/site/doc/pages/has.txt index 20eaaff..ad749f5 100644 --- a/site/doc/pages/has.txt +++ b/site/doc/pages/has.txt @@ -1,5 +1,5 @@ :sig -bool DTree::has(String s) const +bool DValue::has(String s) const :params s : child key to test @@ -7,13 +7,13 @@ return value : true when the node is map-shaped and contains the key :see >types -0_DTree +0_DValue get_by_path each is_array :content -Tests whether a map-shaped `DTree` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Tests whether a map-shaped `DValue` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. Returns `false` for scalar nodes and for missing keys. diff --git a/site/doc/pages/is_array.txt b/site/doc/pages/is_array.txt index 3bf8680..59c7013 100644 --- a/site/doc/pages/is_array.txt +++ b/site/doc/pages/is_array.txt @@ -1,26 +1,26 @@ :sig -bool DTree::is_array() const +bool DValue::is_array() const :params return value : true when the node is map-shaped :see >types -0_DTree +0_DValue is_list has each :content -Tests whether a `DTree` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Tests whether a `DValue` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. `is_array()` is true for both list-like and keyed containers; use `is_list()` to distinguish the two: ```cpp -DTree rows = sqlite_query(db, "select * from notes"); +DValue rows = sqlite_query(db, "select * from notes"); if(rows.is_array()) { - rows.each([&](const DTree& row, String key) { + rows.each([&](const DValue& row, String key) { // ... }); } diff --git a/site/doc/pages/is_list.txt b/site/doc/pages/is_list.txt index 7209bc8..ff9af51 100644 --- a/site/doc/pages/is_list.txt +++ b/site/doc/pages/is_list.txt @@ -1,24 +1,24 @@ :sig -bool DTree::is_list() const +bool DValue::is_list() const :params return value : true when the node is a sequential, numerically indexed container :see >types -0_DTree +0_DValue is_array push each -dtree_values +dv_values :content -Tests whether a map-shaped `DTree` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Tests whether a map-shaped `DValue` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. Containers built with `push()` are lists. An empty container counts as a list when it was created with `set_array()` or `push()`; a keyed map (or a map with gaps in its numeric keys) is `is_array()` but not `is_list()`. ```cpp -DTree items; +DValue items; items.push(first_item); items.push(second_item); // items.is_list() == true @@ -27,7 +27,7 @@ items["custom"] = "x"; // items.is_list() == false, items.is_array() == true ``` -`dtree_map()` and `dtree_filter()` use this distinction to decide whether results re-index from zero or keep their original keys. +`dv_map()` and `dv_filter()` use this distinction to decide whether results re-index from zero or keep their original keys. ## Related Concepts diff --git a/site/doc/pages/json_decode.txt b/site/doc/pages/json_decode.txt index 0de4867..09ebbc0 100644 --- a/site/doc/pages/json_decode.txt +++ b/site/doc/pages/json_decode.txt @@ -1,21 +1,21 @@ :sig -DTree json_decode(String s) +DValue json_decode(String s) :params s : string containing JSON data -return value : a DTree object containing the deserialized JSON data +return value : a DValue object containing the deserialized JSON data :see -0_DTree +0_DValue json_encode to_bool to_f64 to_u64 :content -Deserializes `s` into a `DTree` structure. +Deserializes `s` into a `DValue` structure. -The returned structure is usually consumed through `DTree` accessors such as: +The returned structure is usually consumed through `DValue` accessors such as: - `tree["key"].to_string()` - `tree["count"].to_u64()` @@ -23,10 +23,10 @@ The returned structure is usually consumed through `DTree` accessors such as: Current runtime behavior: -- JSON objects and arrays become map-shaped `DTree` values. -- JSON booleans become native `bool` `DTree` values. -- JSON strings become native `String` `DTree` values. -- JSON numbers currently deserialize as string-valued `DTree` nodes, so typed conversions such as `to_f64()` and `to_u64()` are the normal way to read numeric content. +- JSON objects and arrays become map-shaped `DValue` values. +- JSON booleans become native `bool` `DValue` values. +- JSON strings become native `String` `DValue` values. +- JSON numbers currently deserialize as string-valued `DValue` nodes, so typed conversions such as `to_f64()` and `to_u64()` are the normal way to read numeric content. Related: diff --git a/site/doc/pages/json_encode.txt b/site/doc/pages/json_encode.txt index 593830a..41f5a9c 100644 --- a/site/doc/pages/json_encode.txt +++ b/site/doc/pages/json_encode.txt @@ -1,25 +1,25 @@ :sig String json_encode(String s) -String json_encode(DTree t) +String json_encode(DValue t) :params s : string to encode as a JSON string literal -t : DTree object to be serialized +t : DValue object to be serialized return value : string containing the JSON result :see >types json_decode -0_DTree +0_DValue String html_escape :content -Serializes either a `String` or a `DTree` into JSON notation. +Serializes either a `String` or a `DValue` into JSON notation. When passed a `String`, `json_encode()` returns a quoted and escaped JSON string literal. -When passed a `DTree`, scalar values are serialized directly and nested map values are emitted as JSON objects. +When passed a `DValue`, scalar values are serialized directly and nested map values are emitted as JSON objects. Related: diff --git a/site/doc/pages/list_every.txt b/site/doc/pages/list_every.txt index 5e26b8a..2ea8864 100644 --- a/site/doc/pages/list_every.txt +++ b/site/doc/pages/list_every.txt @@ -6,7 +6,7 @@ bool list_every(StringList items, function f) :see StringList -0_DTree +0_DValue filter :content diff --git a/site/doc/pages/list_find.txt b/site/doc/pages/list_find.txt index 3cc88d0..9912f8a 100644 --- a/site/doc/pages/list_find.txt +++ b/site/doc/pages/list_find.txt @@ -6,7 +6,7 @@ String list_find(StringList items, function f, String fallback = :see StringList -0_DTree +0_DValue filter :content diff --git a/site/doc/pages/list_some.txt b/site/doc/pages/list_some.txt index 3fbfa8a..f1ea7a0 100644 --- a/site/doc/pages/list_some.txt +++ b/site/doc/pages/list_some.txt @@ -6,7 +6,7 @@ bool list_some(StringList items, function f) :see StringList -0_DTree +0_DValue filter :content diff --git a/site/doc/pages/list_sort.txt b/site/doc/pages/list_sort.txt index 8aabcde..1d9813f 100644 --- a/site/doc/pages/list_sort.txt +++ b/site/doc/pages/list_sort.txt @@ -6,7 +6,7 @@ StringList list_sort(StringList items) :see StringList -0_DTree +0_DValue filter :content diff --git a/site/doc/pages/list_unique.txt b/site/doc/pages/list_unique.txt index 0f97094..5d59cdf 100644 --- a/site/doc/pages/list_unique.txt +++ b/site/doc/pages/list_unique.txt @@ -6,7 +6,7 @@ StringList list_unique(StringList items) :see StringList -0_DTree +0_DValue filter :content diff --git a/site/doc/pages/map.txt b/site/doc/pages/map.txt index 98272b6..a4e94b6 100644 --- a/site/doc/pages/map.txt +++ b/site/doc/pages/map.txt @@ -14,7 +14,7 @@ return value : a new list containing the transformed values >string filter StringList -0_DTree +0_DValue :content Returns a new list by calling `f` for each item in `items`. diff --git a/site/doc/pages/markdown_to_ast.txt b/site/doc/pages/markdown_to_ast.txt index 48dffc6..6a10fe5 100644 --- a/site/doc/pages/markdown_to_ast.txt +++ b/site/doc/pages/markdown_to_ast.txt @@ -1,21 +1,21 @@ :sig -DTree markdown_to_ast(String src) -DTree markdown_to_ast(String src, DTree options) +DValue markdown_to_ast(String src) +DValue markdown_to_ast(String src, DValue options) :params src : markdown source text options : optional markdown options tree -return value : a `DTree` document AST +return value : a `DValue` document AST :see markdown_to_html component component_render json_encode -0_DTree +0_DValue :content -Parses Markdown source into a structured `DTree` document tree. +Parses Markdown source into a structured `DValue` document tree. The parser targets a practical GitHub-flavored subset by default: @@ -67,8 +67,8 @@ Common inline nodes: Example: ```uce -DTree options = json_decode("{\"components\":{\":::warning\":\"components/markdown/warning\"}}"); -DTree ast = markdown_to_ast(file_get_contents("README.md"), options); +DValue options = json_decode("{\"components\":{\":::warning\":\"components/markdown/warning\"}}"); +DValue ast = markdown_to_ast(file_get_contents("README.md"), options); print(json_encode(ast)); ``` diff --git a/site/doc/pages/markdown_to_html.txt b/site/doc/pages/markdown_to_html.txt index df6c9d3..95ed347 100644 --- a/site/doc/pages/markdown_to_html.txt +++ b/site/doc/pages/markdown_to_html.txt @@ -1,6 +1,6 @@ :sig String markdown_to_html(String src) -String markdown_to_html(String src, DTree options) +String markdown_to_html(String src, DValue options) :params src : markdown source text @@ -26,7 +26,7 @@ By default the function aims at a practical GitHub-flavored Markdown target, inc Example: ```uce -DTree options; +DValue options; options["components"][":::warning"] = "components/markdown/warning"; options["components"]["node.code_block"] = "components/markdown/code_block"; String html = markdown_to_html(file_get_contents("guide.md"), options); diff --git a/site/doc/pages/mysql_query.txt b/site/doc/pages/mysql_query.txt index 298f69f..97dbfdb 100644 --- a/site/doc/pages/mysql_query.txt +++ b/site/doc/pages/mysql_query.txt @@ -1,5 +1,5 @@ :sig -DTree mysql_query(MySQL* m, String q, StringMap params) +DValue mysql_query(MySQL* m, String q, StringMap params) :params m : pointer to an active MySQL connection struct @@ -18,13 +18,13 @@ Executes a MySQL query and returns the resulting data, if any. ```cpp StringMap params; params["email"] = "ada@example.test"; -DTree rows = mysql_query(m, +DValue rows = mysql_query(m, "select id, email from users where email = :email", params ); ``` -The result is returned as a `DTree`, which makes it easy to iterate through rows and read fields with the usual `DTree` accessors. +The result is returned as a `DValue`, which makes it easy to iterate through rows and read fields with the usual `DValue` accessors. Related: diff --git a/site/doc/pages/regex_search.txt b/site/doc/pages/regex_search.txt index 2d3e0b3..580eedd 100644 --- a/site/doc/pages/regex_search.txt +++ b/site/doc/pages/regex_search.txt @@ -1,12 +1,12 @@ :sig -DTree regex_search(String pattern, String subject) -DTree regex_search(String pattern, String subject, String flags) +DValue regex_search(String pattern, String subject) +DValue regex_search(String pattern, String subject, String flags) :params pattern : PCRE2 regular expression pattern subject : string to search flags : optional regex flags -return value : a DTree describing the first match +return value : a DValue describing the first match :see >regex @@ -14,7 +14,7 @@ regex_match regex_search_all regex_replace regex_split -0_DTree +0_DValue :content Searches `subject` for the first occurrence of `pattern` and returns structured match data. @@ -22,7 +22,7 @@ Searches `subject` for the first occurrence of `pattern` and returns structured Example: ```uce -DTree match = regex_search( +DValue match = regex_search( "(?[A-Za-z0-9._%+-]+)@(?[A-Za-z0-9.-]+)", "Contact ops@example.test" ); diff --git a/site/doc/pages/regex_search_all.txt b/site/doc/pages/regex_search_all.txt index 2adfb00..4047ebb 100644 --- a/site/doc/pages/regex_search_all.txt +++ b/site/doc/pages/regex_search_all.txt @@ -1,12 +1,12 @@ :sig -DTree regex_search_all(String pattern, String subject) -DTree regex_search_all(String pattern, String subject, String flags) +DValue regex_search_all(String pattern, String subject) +DValue regex_search_all(String pattern, String subject, String flags) :params pattern : PCRE2 regular expression pattern subject : string to search flags : optional regex flags -return value : a DTree containing all non-overlapping matches +return value : a DValue containing all non-overlapping matches :see >regex @@ -14,7 +14,7 @@ regex_match regex_search regex_replace regex_split -0_DTree +0_DValue :content Finds every non-overlapping match of `pattern` in `subject`. @@ -22,9 +22,9 @@ Finds every non-overlapping match of `pattern` in `subject`. Example: ```uce -DTree tags = regex_search_all("#(?[A-Za-z0-9_]+)", "Ship #uce and #docs"); +DValue tags = regex_search_all("#(?[A-Za-z0-9_]+)", "Ship #uce and #docs"); -tags["matches"].each([](DTree match, String key) { +tags["matches"].each([](DValue match, String key) { print(match["named"]["tag"].to_string(), "\n"); }); ``` diff --git a/site/doc/pages/request_query_route.txt b/site/doc/pages/request_query_route.txt index 3f528dc..278b18a 100644 --- a/site/doc/pages/request_query_route.txt +++ b/site/doc/pages/request_query_route.txt @@ -2,7 +2,7 @@ request_query_route :sig -DTree request_query_route(Request& context, String default_path = "index") +DValue request_query_route(Request& context, String default_path = "index") :see request_query_path @@ -21,7 +21,7 @@ Fields: - `valid`: boolean; true when `l_path` is safe ```cpp -DTree route = request_query_route(context); +DValue route = request_query_route(context); if(route["valid"].to_bool()) print("route=", route["l_path"].to_string()); ``` diff --git a/site/doc/pages/sqlite_query.txt b/site/doc/pages/sqlite_query.txt index 8b307af..8eaa371 100644 --- a/site/doc/pages/sqlite_query.txt +++ b/site/doc/pages/sqlite_query.txt @@ -1,12 +1,12 @@ :sig -DTree sqlite_query(SQLite* db, String q) -DTree sqlite_query(SQLite* db, String q, StringMap params) +DValue sqlite_query(SQLite* db, String q) +DValue sqlite_query(SQLite* db, String q, StringMap params) :params db : pointer to an active SQLite connection q : SQL statement params : optional named parameter map -return value : list of result rows as a DTree +return value : list of result rows as a DValue :see >sqlite @@ -14,10 +14,10 @@ sqlite_connect sqlite_error sqlite_insert_id sqlite_affected_rows -0_DTree +0_DValue :content -Executes one SQLite statement and returns result rows as a `DTree` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement. +Executes one SQLite statement and returns result rows as a `DValue` array. Multi-statement SQL strings are rejected so migrations cannot silently run only their first statement. Use named parameters with `:name` placeholders only. Positional `?` placeholders and SQLite's other named marker forms (`@name`, `$name`) are rejected so UCE SQLite queries use the same placeholder style as the MySQL helper. UCE binds parameters with SQLite prepared statements; it does not substitute values into the SQL string. @@ -26,12 +26,12 @@ SQLite* db = sqlite_connect("/tmp/app.sqlite"); StringMap params; params["email"] = "ada@example.test"; -DTree rows = sqlite_query(db, +DValue rows = sqlite_query(db, "select id, email from users where email = :email", params ); ``` -Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DTree values. Blob values are returned as byte strings. +Result rows are objects keyed by column name. SQLite integer, float, text, blob, and null values are converted to DValue values. Blob values are returned as byte strings. For statements that do not return rows, inspect `sqlite_affected_rows()` or `sqlite_insert_id()` after the call. diff --git a/site/doc/pages/to_bool.txt b/site/doc/pages/to_bool.txt index d5890cf..3c3e982 100644 --- a/site/doc/pages/to_bool.txt +++ b/site/doc/pages/to_bool.txt @@ -1,5 +1,5 @@ :sig -bool DTree::to_bool(bool default_value = false) const +bool DValue::to_bool(bool default_value = false) const :params default_value : returned when the value is missing or empty @@ -7,14 +7,14 @@ return value : the value as a boolean, or `default_value` :see >types -0_DTree +0_DValue json_decode to_f64 to_u64 to_string :content -Reads a `DTree` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Reads a `DValue` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false. Numeric values read as true when non-zero. diff --git a/site/doc/pages/to_f64.txt b/site/doc/pages/to_f64.txt index cbe098c..e937327 100644 --- a/site/doc/pages/to_f64.txt +++ b/site/doc/pages/to_f64.txt @@ -1,5 +1,5 @@ :sig -f64 DTree::to_f64(f64 default_value = 0) const +f64 DValue::to_f64(f64 default_value = 0) const :params default_value : returned when the value is missing or cannot be parsed as a number @@ -7,7 +7,7 @@ return value : the value as a floating-point number, or `default_value` :see >types -0_DTree +0_DValue json_decode float_val to_bool @@ -15,7 +15,7 @@ to_s64 to_u64 :content -Reads a `DTree` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Reads a `DValue` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. String values are trimmed and parsed permissively: numeric forms and the boolean words understood by `to_bool()` both convert. Boolean values become `1.0` or `0.0`. diff --git a/site/doc/pages/to_json.txt b/site/doc/pages/to_json.txt index 2d601d8..0fa1028 100644 --- a/site/doc/pages/to_json.txt +++ b/site/doc/pages/to_json.txt @@ -1,5 +1,5 @@ :sig -String DTree::to_json(char quote_char = '"') const +String DValue::to_json(char quote_char = '"') const :params quote_char : quote character used around string output @@ -7,13 +7,13 @@ return value : the scalar as a single JSON token :see >types -0_DTree +0_DValue json_encode json_decode to_string :content -Renders a single scalar `DTree` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Renders a single scalar `DValue` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. Strings are escaped and quoted, numbers render as numeric literals, and booleans render as `true` / `false`. diff --git a/site/doc/pages/to_s64.txt b/site/doc/pages/to_s64.txt index 7ae4ab7..f7fb05c 100644 --- a/site/doc/pages/to_s64.txt +++ b/site/doc/pages/to_s64.txt @@ -1,5 +1,5 @@ :sig -s64 DTree::to_s64(s64 default_value = 0) const +s64 DValue::to_s64(s64 default_value = 0) const :params default_value : returned when the value is missing or cannot be parsed as a number @@ -7,14 +7,14 @@ return value : the value as a signed 64-bit integer, or `default_value` :see >types -0_DTree +0_DValue to_u64 to_f64 to_bool to_string :content -Reads a `DTree` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Reads a `DValue` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. String values are trimmed and parsed permissively: plain integers, floating-point forms (truncated toward zero), and the boolean words understood by `to_bool()` (`yes` reads as `1`) all convert. Results outside the `s64` range clamp to the range boundaries. Boolean values become `1` or `0`. diff --git a/site/doc/pages/to_string.txt b/site/doc/pages/to_string.txt index 8e56ffb..3452e39 100644 --- a/site/doc/pages/to_string.txt +++ b/site/doc/pages/to_string.txt @@ -1,5 +1,5 @@ :sig -String DTree::to_string(String default_value = "") const +String DValue::to_string(String default_value = "") const :params default_value : returned when the node holds no usable text @@ -7,7 +7,7 @@ return value : the scalar content as text, or `default_value` :see >types -0_DTree +0_DValue to_s64 to_f64 to_bool @@ -15,7 +15,7 @@ to_json get_by_path :content -Reads a `DTree` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Reads a `DValue` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. `default_value` is returned when the value is missing or not text-convertible: diff --git a/site/doc/pages/to_stringmap.txt b/site/doc/pages/to_stringmap.txt index 78a005e..6f957bb 100644 --- a/site/doc/pages/to_stringmap.txt +++ b/site/doc/pages/to_stringmap.txt @@ -1,19 +1,19 @@ :sig -StringMap DTree::to_stringmap() const +StringMap DValue::to_stringmap() const :params return value : a flat `StringMap` projection of the node :see >types -0_DTree +0_DValue StringMap to_string -dtree_keys -dtree_values +dv_keys +dv_values :content -Converts a `DTree` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Converts a `DValue` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. - Map-shaped nodes produce one entry per child, with each child read via `to_string()`. Nested maps flatten to empty strings — this is a one-level projection, not a serializer. - A non-empty scalar produces a single `"value"` entry holding the scalar. @@ -25,7 +25,7 @@ Use this when handing request- or config-shaped data to APIs that take `StringMa ```cpp StringMap params = context.props["filters"].to_stringmap(); -DTree rows = sqlite_query(db, "select * from notes where author = :author", params); +DValue rows = sqlite_query(db, "select * from notes where author = :author", params); ``` For a faithful representation of nested data, use `json_encode()` instead. diff --git a/site/doc/pages/to_u64.txt b/site/doc/pages/to_u64.txt index 09b2dd6..d532a03 100644 --- a/site/doc/pages/to_u64.txt +++ b/site/doc/pages/to_u64.txt @@ -1,5 +1,5 @@ :sig -u64 DTree::to_u64(u64 default_value = 0) const +u64 DValue::to_u64(u64 default_value = 0) const :params default_value : returned when the value is missing or cannot be parsed as a number @@ -7,7 +7,7 @@ return value : the value as an unsigned 64-bit integer, or `default_value` :see >types -0_DTree +0_DValue json_decode int_val to_bool @@ -15,7 +15,7 @@ to_s64 to_f64 :content -Reads a `DTree` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. +Reads a `DValue` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically. String values are trimmed and parsed permissively, like `to_s64()`. Boolean values become `1` or `0`. Negative values clamp to `0`, and results above the `u64` range clamp to the maximum. @@ -34,7 +34,7 @@ u64 limit = context.get["limit"].to_u64(50); u64 owner_id = row.get_by_path("owner/id").to_u64(); ``` -Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DTree`. +Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DValue`. ## Related Concepts diff --git a/site/doc/pages/unit_call.txt b/site/doc/pages/unit_call.txt index 62063e2..22bf6d2 100644 --- a/site/doc/pages/unit_call.txt +++ b/site/doc/pages/unit_call.txt @@ -1,11 +1,11 @@ :sig -DTree* unit_call(String file_name, String function_name, DTree* call_param = null) +DValue* unit_call(String file_name, String function_name, DValue* call_param = null) :params file_name : UCE file to load and execute function_name : name of the function to invoke call_param : optional, call parameter -return value : DTree* returned from function +return value : DValue* returned from function :see >ob @@ -20,7 +20,7 @@ Calls an exported function inside another UCE file. Use `unit_call()` when you need structured data exchange between units rather than rendered HTML output. -The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DTree*` owned by the callee. +The callee must expose an `EXPORT` function whose name matches `function_name`. Arguments are passed through `call_param`, and the return value is a `DValue*` owned by the callee. `unit_call()` also understands the request-bound UCE entrypoint names: @@ -31,7 +31,7 @@ The callee must expose an `EXPORT` function whose name matches `function_name`. - `ONCE` - `INIT` -When `function_name` matches one of those macro-style entrypoints, `unit_call()` does not look for a plain `EXPORT DTree* ...` function. Instead, it translates the name to the generated C++ symbol, uses the current `Request` context, and passes `call_param` into `context.props`, matching the normal component invocation model. +When `function_name` matches one of those macro-style entrypoints, `unit_call()` does not look for a plain `EXPORT DValue* ...` function. Instead, it translates the name to the generated C++ symbol, uses the current `Request` context, and passes `call_param` into `context.props`, matching the normal component invocation model. For `RENDER...` and `COMPONENT...`, the unit's `ONCE(Request& context)` hook is still honored automatically before the selected handler runs. @@ -39,7 +39,7 @@ Example: ```cpp // export a function -EXPORT DTree* test_func(DTree* call_param) +EXPORT DValue* test_func(DValue* call_param) { print("HELLO FROM TEST FUNCTION"); return(0); @@ -52,7 +52,7 @@ unit_call("call_file_funcs.uce", "test_func"); Calling a named component handler through `unit_call()`: ```cpp -DTree props; +DValue props; props["title"] = "Diagnostics"; props["body"] = "Ready"; @@ -62,7 +62,7 @@ unit_call("components/card.uce", "COMPONENT:BODY", &props); Calling a page render handler through `unit_call()`: ```cpp -DTree props; +DValue props; props["section"] = "summary"; unit_call("reports/summary.uce", "RENDER", &props); diff --git a/site/doc/pages/unit_info.txt b/site/doc/pages/unit_info.txt index 4963902..71e1c24 100644 --- a/site/doc/pages/unit_info.txt +++ b/site/doc/pages/unit_info.txt @@ -1,5 +1,5 @@ :sig -DTree unit_info(String path = "") +DValue unit_info(String path = "") :params path : optional UCE unit path. If empty, uses the current executing unit. diff --git a/site/doc/pages/var_dump.txt b/site/doc/pages/var_dump.txt index 5758268..451c698 100644 --- a/site/doc/pages/var_dump.txt +++ b/site/doc/pages/var_dump.txt @@ -1,7 +1,7 @@ :sig String var_dump(StringMap t, String prefix = "", String postfix = "\n") String var_dump(StringList t, String prefix = "", String postfix = "\n") -String var_dump(DTree t, String prefix = "", String postfix = "\n") +String var_dump(DValue t, String prefix = "", String postfix = "\n") :params t : object to be dumped into a string @@ -9,7 +9,7 @@ return value : string containing a human-friendly representation of 't' :see >types -0_DTree +0_DValue StringMap json_encode print diff --git a/site/doc/pages/xml_decode.txt b/site/doc/pages/xml_decode.txt index 6c1f2ee..f4a1dae 100644 --- a/site/doc/pages/xml_decode.txt +++ b/site/doc/pages/xml_decode.txt @@ -1,19 +1,19 @@ :sig -DTree xml_decode(String s) +DValue xml_decode(String s) :params s : XML source string -return value : element-shaped DTree +return value : element-shaped DValue :see >markup xml_encode json_decode -0_DTree +0_DValue String :content -Parses a simple XML document into a structured `DTree`. +Parses a simple XML document into a structured `DValue`. `xml_decode()` is intentionally small. It does not validate schemas, DTDs, namespaces, or document types. It parses the first root element and returns the same structural element shape accepted by `xml_encode()`. @@ -31,7 +31,7 @@ node["children"] = list of child element nodes Example: ```uce -DTree book = xml_decode("UCE & XML"); +DValue book = xml_decode("UCE & XML"); book["name"].to_string(); // book book["attrs"]["id"].to_string(); // b1 @@ -42,7 +42,7 @@ book["children"]["0"]["text"].to_string(); // UCE & XML CDATA and numeric entities are folded into text: ```uce -DTree note = xml_decode("AB"); +DValue note = xml_decode("AB"); note["text"].to_string(); // 5 < 6 note["children"]["0"]["text"].to_string(); // AB diff --git a/site/doc/pages/xml_encode.txt b/site/doc/pages/xml_encode.txt index c8babfc..a932b43 100644 --- a/site/doc/pages/xml_encode.txt +++ b/site/doc/pages/xml_encode.txt @@ -1,6 +1,6 @@ :sig -String xml_encode(DTree t) -String xml_encode(DTree t, String root_name) +String xml_encode(DValue t) +String xml_encode(DValue t, String root_name) :params t : tree to serialize as XML @@ -11,11 +11,11 @@ return value : XML string >markup xml_decode json_encode -0_DTree +0_DValue html_escape :content -Serializes a `DTree` into a simple XML string. +Serializes a `DValue` into a simple XML string. `xml_encode()` does not validate against a schema, DTD, or namespace rules. It is a structural converter for application data, similar in spirit to `json_encode()`. @@ -33,11 +33,11 @@ node["children"] = list of child element nodes Example: ```uce -DTree book; +DValue book; book["name"] = "book"; book["attrs"]["id"] = "b1"; -DTree title; +DValue title; title["name"] = "title"; title["text"] = "UCE & XML"; book["children"].push(title); @@ -54,7 +54,7 @@ The result is: For simple map/list/scalar trees, `xml_encode()` creates ordinary child elements: ```uce -DTree payload; +DValue payload; payload["title"] = "Hello"; payload["count"] = "3"; @@ -73,4 +73,4 @@ Notes: - Text and attribute values are escaped. - List values use repeated `` children. - Empty elements serialize as self-closing tags. -- Map child order follows `DTree` map iteration order. +- Map child order follows `DValue` map iteration order. diff --git a/site/doc/pages/yaml_decode.txt b/site/doc/pages/yaml_decode.txt index b19f358..259c4c4 100644 --- a/site/doc/pages/yaml_decode.txt +++ b/site/doc/pages/yaml_decode.txt @@ -1,19 +1,19 @@ :sig -DTree yaml_decode(String s) +DValue yaml_decode(String s) :params s : YAML source string -return value : decoded DTree +return value : decoded DValue :see >markup yaml_encode json_decode xml_decode -0_DTree +0_DValue :content -Parses a practical YAML subset into a `DTree`. +Parses a practical YAML subset into a `DValue`. `yaml_decode()` is designed for concise UCE config files. It intentionally avoids full YAML schema behavior and does not support anchors, aliases, tags, directives, or complex inline collection syntax. @@ -30,7 +30,7 @@ String source = "app:\n" " - site\n" " - cache\n"; -DTree cfg = yaml_decode(source); +DValue cfg = yaml_decode(source); cfg["app"]["name"].to_string(); // UCE Starter cfg["app"]["debug"].to_bool(); // true diff --git a/site/doc/pages/yaml_encode.txt b/site/doc/pages/yaml_encode.txt index 6524ba7..619d000 100644 --- a/site/doc/pages/yaml_encode.txt +++ b/site/doc/pages/yaml_encode.txt @@ -1,5 +1,5 @@ :sig -String yaml_encode(DTree t) +String yaml_encode(DValue t) :params t : tree to serialize as YAML @@ -10,10 +10,10 @@ return value : YAML string yaml_decode json_encode xml_encode -0_DTree +0_DValue :content -Serializes a `DTree` into a compact YAML string for configuration files. +Serializes a `DValue` into a compact YAML string for configuration files. `yaml_encode()` focuses on the ordinary data shapes UCE apps use for config: nested maps, lists, strings, booleans, and numeric `f64` values. It does not emit YAML tags, anchors, aliases, or custom schema features. @@ -22,12 +22,12 @@ Try the live example in the [YAML demo](../demo/yaml.uce). Example: ```uce -DTree cfg; +DValue cfg; cfg["app"]["name"] = "UCE Starter"; cfg["app"]["debug"].set_bool(true); cfg["app"]["port"] = (f64)8080; -DTree path; +DValue path; path = "site"; cfg["app"]["paths"].push(path); path = "cache"; @@ -50,8 +50,8 @@ app: Notes: -- Map keys are emitted in `DTree` map iteration order. +- Map keys are emitted in `DValue` map iteration order. - Strings are left plain when safe and quoted when needed. - Multiline strings are emitted as literal block scalars with `|`. - Empty strings are emitted as `""` so they are not confused with YAML null. -- Decoded numeric-looking config values are strings unless the original `DTree` value was explicitly numeric. +- Decoded numeric-looking config values are strings unless the original `DValue` value was explicitly numeric. diff --git a/site/doc/pages/zip_create.txt b/site/doc/pages/zip_create.txt index 7995d2f..36fb817 100644 --- a/site/doc/pages/zip_create.txt +++ b/site/doc/pages/zip_create.txt @@ -1,5 +1,5 @@ :sig -bool zip_create(String zip_file_name, DTree entries) +bool zip_create(String zip_file_name, DValue entries) :params zip_file_name : path to the archive to create @@ -11,7 +11,7 @@ return value : `true` when the archive is written zip_list zip_read zip_extract -DTree +DValue :content Creates a ZIP archive at `zip_file_name`. @@ -19,7 +19,7 @@ Creates a ZIP archive at `zip_file_name`. `entries` can be a simple map where each key is the archive member name and each value is the file content: ```uce -DTree entries; +DValue entries; entries["hello.txt"] = "Hello ZIP"; entries["nested/readme.txt"] = "Nested file"; zip_create("/tmp/example.zip", entries); @@ -28,7 +28,7 @@ zip_create("/tmp/example.zip", entries); For list-shaped input or when the map key should not be the member name, each child can provide explicit fields: ```uce -DTree item; +DValue item; item["name"] = "data/value.txt"; item["content"] = "42"; entries["ignored-key"] = item; diff --git a/site/doc/pages/zip_list.txt b/site/doc/pages/zip_list.txt index 9fbf04a..d017634 100644 --- a/site/doc/pages/zip_list.txt +++ b/site/doc/pages/zip_list.txt @@ -1,16 +1,16 @@ :sig -DTree zip_list(String zip_file_name) +DValue zip_list(String zip_file_name) :params zip_file_name : path to the ZIP archive to inspect -return value : DTree containing archive metadata and an `entries` array +return value : DValue containing archive metadata and an `entries` array :see >sys zip_create zip_read zip_extract -DTree +DValue :content Reads the central directory from `zip_file_name` and returns structured metadata. @@ -33,6 +33,6 @@ Each entry contains: Example: ```uce -DTree info = zip_list("/tmp/example.zip"); +DValue info = zip_list("/tmp/example.zip"); print(json_encode(info)); ``` diff --git a/site/errors/compiler-error.uce b/site/errors/compiler-error.uce index 5bd451e..ab9698d 100644 --- a/site/errors/compiler-error.uce +++ b/site/errors/compiler-error.uce @@ -6,7 +6,7 @@ RENDER(Request& context) { - DTree error = context.call["error"]; + DValue error = context.call["error"]; <> diff --git a/site/errors/runtime-error.uce b/site/errors/runtime-error.uce index 9a4afe3..fe913b8 100644 --- a/site/errors/runtime-error.uce +++ b/site/errors/runtime-error.uce @@ -6,7 +6,7 @@ RENDER(Request& context) { - DTree error = context.call["error"]; + DValue error = context.call["error"]; String signal_name = error["signal_name"].to_string(); <> diff --git a/site/examples/uce-starter/components/auth/oauth-client.uce b/site/examples/uce-starter/components/auth/oauth-client.uce index 263d081..2e4f806 100644 --- a/site/examples/uce-starter/components/auth/oauth-client.uce +++ b/site/examples/uce-starter/components/auth/oauth-client.uce @@ -6,7 +6,7 @@ COMPONENT(Request& context) String subtitle = first(context.props["subtitle"].to_string(), "Choose your preferred authentication method"); String callback_url = first(context.props["callback_url"].to_string(), app_link("auth/callback", context)); - DTree services = context.props["services"]; + DValue services = context.props["services"]; if(services.get_type_name() != "array" || services["google"]["name"].to_string() == "") { services["google"]["name"] = "Google"; @@ -40,7 +40,7 @@ COMPONENT(Request& context)

    -
    - " class="u-sortable-table"> - > " class="muted"> - " data-sort-value=""> @@ -204,7 +204,7 @@ COMPONENT:FEATURES_GRID(Request& context)

    Discover the powerful features that make development a breeze

    -

    @@ -288,10 +288,10 @@ COMPONENT:FEATURES_GRID(Request& context) COMPONENT:STATS_SECTION(Request& context) { - DTree stats = context.props["stats"]; + DValue stats = context.props["stats"]; if(stats.get_type_name() != "array" || stats["0"]["label"].to_string() == "") { - DTree item; + DValue item; item["number"] = "99.9%"; item["label"] = "Uptime"; stats.push(item); item.clear(); item["number"] = "500ms"; item["label"] = "Average Response"; stats.push(item); item.clear(); item["number"] = "50K+"; item["label"] = "Active Users"; stats.push(item); item.clear(); @@ -306,7 +306,7 @@ COMPONENT:STATS_SECTION(Request& context)

    Join thousands of developers who have chosen our framework

    -
    @@ -364,10 +364,10 @@ COMPONENT:STATS_SECTION(Request& context) COMPONENT:BRANDS_SHOWCASE(Request& context) { - DTree logos = context.props["logos"]; + DValue logos = context.props["logos"]; if(logos.get_type_name() != "array" || logos["0"]["name"].to_string() == "") { - DTree item; + DValue item; for(s32 i = 1; i <= 6; i++) { item["name"] = "Brand " + std::to_string(i); @@ -383,7 +383,7 @@ COMPONENT:BRANDS_SHOWCASE(Request& context)

    -
    " alt="" />
    Don't just take our word for it - hear from the community

    -
    "
    @@ -473,11 +473,11 @@ COMPONENT:TESTIMONIALS(Request& context) COMPONENT:PRICING_TABLE(Request& context) { - DTree plans = context.props["plans"]; + DValue plans = context.props["plans"]; if(plans.get_type_name() != "array" || plans["0"]["name"].to_string() == "") { - DTree plan; - DTree feature; + DValue plan; + DValue feature; plan["name"] = "Starter"; plan["price"] = "Free"; plan["period"] = ""; plan["description"] = "Perfect for small projects and learning"; plan["cta"] = "Start Free"; feature = "Up to 3 projects"; plan["features"].push(feature); feature = "Community support"; plan["features"].push(feature); feature = "Basic components"; plan["features"].push(feature); plans.push(plan); plan.clear(); plan["name"] = "Pro"; plan["price"] = "$24"; plan["period"] = "/mo"; plan["description"] = "For serious products and teams"; plan["cta"] = "Choose Pro"; plan["popular"].set_bool(true); feature = "Unlimited projects"; plan["features"].push(feature); feature = "Priority support"; plan["features"].push(feature); feature = "Advanced components"; plan["features"].push(feature); plans.push(plan); plan.clear(); @@ -492,7 +492,7 @@ COMPONENT:PRICING_TABLE(Request& context)

    Choose the plan that fits your product stage.

    -
      -
    • +