Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions docs/roadmap/CVCGL-UI-DSL-ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3278,10 +3278,23 @@ render/resolver thread**. wasm: `emscripten_fetch` is async-only (no sync on the
> **`(http-get url)`** is the transparent one-shot: it self-parks and yields the dict straight into the
> enclosing expression — `(get-attr (http-get url) "status")`, no `msg-recv`. Tests: AriadneNetIntrinsics
> gains the transparent-verb + await-future cases; the msg-recv/await refactor is regression-clean
> across the state_exec suites. **NOT YET:** chunked/streaming bodies; the dedicated libcurl-`multi`
> I/O thread (today each in-flight fetch blocks one compute-pool worker); making ALL ari-generated URI
> requests async (the resolver — separate PR); render-pass park budget (separate PR). NATIVE only until
> the wasm worker/`-sASYNCIFY` fetch path is confirmed.
> across the state_exec suites.
>
> **LANDED (generic async resolver) — `(fetch uri)` / `(fetch-async uri)`.** `net_intrinsics.cpp`
> registers a scheme-agnostic async resolver: it runs the synchronous `resolve()` on the compute pool
> (OFF the scheduler thread) and, like `http-get`, either self-parks (`fetch`) or returns a future
> (`fetch-async`), resuming with `{ ok body(bytes) url error }`. So an ari PROGRAM can async-load ANY
> registered scheme — `file`/`state`/`cvc`/`http` — via one primitive: `(get-attr (fetch uri) "body")`.
> It reuses the PR-A launch+park machinery and needs no HTTP backend (only `http-get*` do), so it works
> even in a curl-less build. Tests: AriadneNetIntrinsics fetch-transparent + fetch-async-future over a
> canned custom scheme, offline. **The other ari-generated URI requests stay synchronous by design:**
> `import:`/`load:`/`include:` resolve at LOAD time, off the draw walk (roadmap §13.8 — sync is legal
> there), and scene `source:{uri:}` is a one-shot at scene-setup (not per-frame). So nothing
> ari-generated blocks the render thread today; a DSL program that wants async loading uses `(fetch)`.
> **NOT YET:** chunked/streaming bodies; the dedicated libcurl-`multi` I/O thread (today each in-flight
> fetch blocks one compute-pool worker); an async scene `source:{uri:}` realize (so a large remote
> asset doesn't block scene setup — a follow-up); render-pass park budget (separate PR). NATIVE only
> until the wasm worker/`-sASYNCIFY` fetch path is confirmed.

### 13.9 A cvc::app-wide HTTP(s) cache in the state tree (TTL + conditional GET) — *planned*

Expand Down
109 changes: 94 additions & 15 deletions src/cvc/ariadne/net_intrinsics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <cvc/ariadne/ariadne.h> // register_action_intrinsics, have_state_exec
#include <cvc/ariadne/net_intrinsics.h>
#include <cvc/ariadne/uri.h> // resolve() — the generic (fetch uri) async resolver
#include <cvc/core/app.h>
#include <cvc/net/http_client.h>

Expand Down Expand Up @@ -92,37 +93,115 @@ std::string launch_http_fetch(cvc::app &app, const std::string &root,
return done;
}

// Marshal a resolver UriResult into the DSL reply dict (scheme-agnostic — no HTTP status/headers).
// The body is `bytes` (opaque octets), consistent with (http-get)'s body.
se::value_t marshal_uri_result(const UriResult &r) {
return se::make_dict({
{"ok", se::value_t(r.ok)},
{"body", se::make_bytes(r.content)},
{"url", se::value_t(r.canonical)},
{"error", se::value_t(r.error)},
});
}

// The generic async resolve (any registered scheme: file/state/cvc/http). Runs the SYNCHRONOUS
// resolve() on a compute-pool worker (OFF the scheduler thread) and posts the marshalled dict to a
// unique '#'-reply channel — the same launch shape as launch_http_fetch, for arbitrary URIs.
std::string launch_uri_fetch(cvc::app &app, const std::string &root,
std::span<const se::value_t> args) {
std::string uri;
if (!args.empty())
if (const std::string *u = std::get_if<std::string>(&args[0].v))
uri = *u;
std::string base; // optional second arg: the base for a relative URI
if (args.size() > 1)
if (const std::string *b = std::get_if<std::string>(&args[1].v))
base = *b;

static std::atomic<std::uint64_t> seq{0};
const std::string chan =
"uri.reply#" + std::to_string(seq.fetch_add(1, std::memory_order_relaxed));
const std::string done = se::resolve_channel_key(root, chan);
app.compute_async(
1, [](int) {},
[&app, uri, base, done] {
se::value_t payload;
try {
payload =
marshal_uri_result(resolve(uri, base)); // resolve() is app-free + has its own barrier
} catch (const std::exception &e) {
UriResult er;
er.ok = false;
er.canonical = uri;
er.error = std::string("fetch: ") + e.what();
payload = marshal_uri_result(er);
} catch (...) {
UriResult er;
er.ok = false;
er.canonical = uri;
er.error = "fetch: unknown error";
payload = marshal_uri_result(er);
}
app.exec_scheduler().post_message(done, payload);
});
return done;
}

} // namespace

void register_net_intrinsics(cvc::app &app) {
if (!have_state_exec() || !cvc::net::have_http_backend())
if (!have_state_exec())
return;
// Warm the lazy per-app singletons on THIS thread before any background worker touches them, so a
// worker never races their first construction (the nav_compute discipline).
app.computePool();
app.exec_scheduler();
// (http-get*) need a compiled cvc::net backend; (fetch*) resolve over any registered scheme
// (file/state/cvc/http) and are useful with or without an HTTP backend.
const bool have_http = cvc::net::have_http_backend();

register_action_intrinsics([&app](std::shared_ptr<se::environment> env,
se::intrinsics_context &ictx) {
register_action_intrinsics([&app, have_http](std::shared_ptr<se::environment> env,
se::intrinsics_context &ictx) {
// Capture the lane's chroot so the reply channel is scoped the same way an (msg-recv)/(await)
// resolves it (launch_http_fetch uses a UNIQUE '#'-suffixed channel per call — policy-exempt +
// resolves it (the launchers use a UNIQUE '#'-suffixed channel per call — policy-exempt +
// identity — so two in-flight fetches never collide on the single recv_path a process has).
const std::string root = ictx.root_path;

// (http-get-async URL [HEADERS]) — kicks the fetch and returns a FUTURE handle; the program
// awaits it with (await …) or (msg-recv …). The low-level primitive for fanning out N fetches.
se::builtins::register_fn(env, "http-get-async",
if (have_http) {
// (http-get-async URL [HEADERS]) — kicks the fetch and returns a FUTURE handle; the program
// awaits it with (await …) or (msg-recv …). The low-level primitive for fanning out N
// fetches.
se::builtins::register_fn(env, "http-get-async",
[&app, root](std::span<const se::value_t> args) -> se::value_t {
return se::make_future(launch_http_fetch(app, root, args));
});

// (http-get URL [HEADERS]) — TRANSPARENT: kicks the fetch and SELF-PARKS the calling process,
// resuming with the response dict threaded straight into the enclosing expression (no
// channel, no msg-recv). Reaches the scheduler via the captured app;
// current_pid()/current_process() are valid here (set around the evaluator step). Equivalent
// to (await (http-get-async URL)).
se::builtins::register_fn(env, "http-get",
[&app, root](std::span<const se::value_t> args) -> se::value_t {
const std::string done = launch_http_fetch(app, root, args);
auto &sched = app.exec_scheduler();
return se::park_on_channel(&sched, sched.current_process().get(),
sched.current_pid(), done);
});
}

// (fetch-async URI [BASE]) / (fetch URI [BASE]) — the GENERIC async resolver over ANY
// registered scheme (file/state/cvc/http). fetch-async returns a future; fetch is transparent
// (self-parks, returns the dict). resolve() runs on the compute pool, so an ari program
// async-loads any URI without blocking the scheduler thread. Reply dict: { ok body(bytes) url
// error }.
se::builtins::register_fn(env, "fetch-async",
[&app, root](std::span<const se::value_t> args) -> se::value_t {
return se::make_future(launch_http_fetch(app, root, args));
return se::make_future(launch_uri_fetch(app, root, args));
});

// (http-get URL [HEADERS]) — TRANSPARENT: kicks the fetch and SELF-PARKS the calling process,
// resuming with the response dict threaded straight into the enclosing expression (no channel,
// no msg-recv). Reaches the scheduler via the captured app; current_pid()/current_process() are
// valid here (set around the evaluator step). Equivalent to (await (http-get-async URL)).
se::builtins::register_fn(env, "http-get",
se::builtins::register_fn(env, "fetch",
[&app, root](std::span<const se::value_t> args) -> se::value_t {
const std::string done = launch_http_fetch(app, root, args);
const std::string done = launch_uri_fetch(app, root, args);
auto &sched = app.exec_scheduler();
return se::park_on_channel(&sched, sched.current_process().get(),
sched.current_pid(), done);
Expand Down
70 changes: 70 additions & 0 deletions src/cvc/tests/ariadne_runtime_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <cvc/ariadne/backend.h>
#include <cvc/ariadne/loader.h> // §12 end-to-end load: mount through the real Runtime
#include <cvc/ariadne/net_intrinsics.h> // §13.8 async (http-get-async) intrinsic test
#include <cvc/ariadne/uri.h> // §13.8 (fetch uri): register a custom scheme handler
#include <cvc/ariadne/widget.h>
#include <cvc/core/app.h>
#include <cvc/core/state.h>
Expand Down Expand Up @@ -2390,3 +2391,72 @@ TEST(AriadneNetIntrinsics, AwaitResolvesHttpGetAsyncFuture) {
EXPECT_EQ(cvc::state::instance(app)("r.status").value(), "200");
EXPECT_EQ(fake->calls.load(), 1);
}

// PR-B: the GENERIC async resolver — (fetch uri) resolves ANY registered scheme off-thread and
// self-parks, returning { ok body(bytes) url error }. No HTTP backend needed (a custom scheme
// here).
TEST(AriadneNetIntrinsics, FetchTransparentResolvesAnyScheme) {
if (!have_state_exec())
GTEST_SKIP() << "libcvc built without state_exec";
cvc::app app;
struct Teardown {
~Teardown() {
clear_action_intrinsics();
unregister_uri_handler("mem");
}
} td;
// A canned custom scheme; the handler runs on the compute-pool worker, so it must be thread-safe
// (a pure lambda returning a fixed UriResult is).
register_uri_handler("mem", [](const Uri &u, const std::string &) {
return UriResult{true, "hello", u.raw, std::string()};
});
register_net_intrinsics(app);

Runtime rt(app, "");
MockBackend mb;
rt.set_backend(&mb);
rt.set_root(group({button("Go", "(begin"
" (set r (fetch \"mem://x\"))"
" (state-set \"r.ok\" (get-attr r \"ok\"))"
" (state-data-set \"r.body\" (get-attr r \"body\")))")}));
mb.button_click = true;
rt.render();
rt.drain();
mb.button_click = false;

ASSERT_TRUE(pump_until(rt, [&] { return !cvc::state::instance(app)("r.ok").value().empty(); }))
<< "(fetch …) never resolved";
EXPECT_EQ(cvc::state::instance(app)("r.ok").value(), "true");
EXPECT_EQ(node_data_string(app, "r.body"), "hello"); // resolved bytes threaded in transparently
}

// PR-B: (await (fetch-async uri)) resolves the generic resolver's future.
TEST(AriadneNetIntrinsics, FetchAsyncFutureAwaited) {
if (!have_state_exec())
GTEST_SKIP() << "libcvc built without state_exec";
cvc::app app;
struct Teardown {
~Teardown() {
clear_action_intrinsics();
unregister_uri_handler("mem");
}
} td;
register_uri_handler("mem", [](const Uri &u, const std::string &) {
return UriResult{true, "world", u.raw, std::string()};
});
register_net_intrinsics(app);

Runtime rt(app, "");
MockBackend mb;
rt.set_backend(&mb);
rt.set_root(group({button("Go", "(state-data-set \"r.body\" "
"(get-attr (await (fetch-async \"mem://y\")) \"body\"))")}));
mb.button_click = true;
rt.render();
rt.drain();
mb.button_click = false;

ASSERT_TRUE(pump_until(rt, [&] { return !node_data_string(app, "r.body").empty(); }))
<< "(await (fetch-async …)) never resolved";
EXPECT_EQ(node_data_string(app, "r.body"), "world");
}
Loading