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
18 changes: 18 additions & 0 deletions docs/roadmap/CVCGL-UI-DSL-ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3249,6 +3249,24 @@ render/resolver thread**. wasm: `emscripten_fetch` is async-only (no sync on the
`fetch_sync()` is unavailable there — the async path is identical across backends. Returns
`resource{kind::bytes|local_path}` to slot into §13.4's dispatch-by-kind.

> **LANDED (first slice) — the async `(http-get-async)` state_exec intrinsic.**
> `ariadne/net_intrinsics.cpp` + `register_net_intrinsics(cvc::app&)` (inc/cvc/ariadne/net_intrinsics.h)
> bind a program-lane verb `(http-get-async URL [HEADERS])` that runs the blocking `cvc::net::send`
> on `app.computePool()` (OFF the scheduler thread) and posts the response to a UNIQUE `#`-suffixed
> reply channel it RETURNS; a program awaits with the existing `(msg-recv <chan>)`, resuming with a
> dict `{ ok status body(bytes) url headers(list) error }`. This is the nav_compute pattern
> (host-intrinsic → `compute_async` → `exec_scheduler().post_message` → parked `msg-recv` woken by
> `drain_ingress`, the delivered value threaded into the enclosing expression). The verb ALWAYS posts
> a dict (an error dict on failure) so a parked recv never hangs; the body is `bytes` (the §13.9
> precursor bridge). Registered host-level (needs `cvc::net` + the app), not a core builtin. Tests in
> `ariadne_runtime_test` (AriadneNetIntrinsics: full-dict await, error-path resume, and a
> BlockingHttpClient proof that the scheduler is not blocked while the fetch is in flight), offline via
> the PR1 `set_http_client` fake. **NOT YET:** a *transparent* `(http-get url)` that returns the body
> directly (needs a new evaluator park-token hook — a host `native_fn` gets no `intrinsics_context` at
> call time, so it can't self-park); chunked/streaming bodies; and the dedicated libcurl-`multi` I/O
> thread (today each in-flight fetch blocks one compute-pool worker). 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*

Repeated `import:`/`load:`/`source:` of the same `http(s)://` URL must not re-fetch every time. The
Expand Down
44 changes: 44 additions & 0 deletions inc/cvc/ariadne/net_intrinsics.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef CVC_ARIADNE_NET_INTRINSICS_H
#define CVC_ARIADNE_NET_INTRINSICS_H

// Ariadne — async HTTP host intrinsics for the state_exec program lanes (roadmap §13.8). Registers
// an `(http-get-async URL [HEADERS])` verb that fetches over cvc::net OFF the scheduler thread and,
// on completion, posts the response to a UNIQUE reply channel the verb RETURNS. A program awaits it
// with the existing `(msg-recv <chan>)`, which resumes with a dict:
//
// (get-attr (msg-recv (http-get-async "https://host/x")) "body")
// ; => dict { ok status body(bytes) url headers(list) error }
//
// WHY two-step (verb + msg-recv) and not a transparent `(http-get url)` that returns the body: a
// host verb is a native_fn(std::span<value_t>) with NO intrinsics_context at call time, so it
// cannot read the calling process's pid or self-park; only a core intrinsic (like msg-recv) can. So
// the verb SUBMITS and returns the channel, and msg-recv (a core intrinsic) does the parking +
// value-threading — the exact nav_compute pattern. A transparent single-verb await needs a new
// evaluator hook (a future item).
//
// THREADING: the fetch runs on app.computePool() (a background worker), so the scheduler/host
// thread is never blocked; the worker touches only the one thread-safe seam,
// exec_scheduler().post_message. The verb ALWAYS posts a dict (an error dict on a transport/arg
// failure), so a parked (msg-recv) never hangs forever.
//
// LANES + LIMITS: use it only in a program `on:`/`on:tick`/`on_key`/`on_pointer` lane — NOT in
// `init:` (no per-frame pump, so a park never resumes) and NOT in the reactive read lane
// (visible_when/computed, default-deny). NATIVE ONLY for now: the background-worker model assumes
// native threads (wasm needs the §13.8 async-fetch path). A no-op without a compiled cvc::net
// backend or without state_exec.

namespace cvc {
class app;
namespace ariadne {

// Register the async net intrinsics against `app` (used for the compute pool + the scheduler
// ingress the completion posts to). Appends to the process-global action-intrinsic providers, so
// call it once at setup, before load_*; tear down with cvc::ariadne::clear_action_intrinsics()
// before `app` dies (the provider captures `app` by reference). A no-op if this build lacks
// state_exec or an HTTP backend.
void register_net_intrinsics(cvc::app &app);

} // namespace ariadne
} // namespace cvc

#endif // CVC_ARIADNE_NET_INTRINSICS_H
3 changes: 2 additions & 1 deletion src/cvc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ list(APPEND INCLUDE_FILES ../../inc/cvc/net/http_client.h)
# the retained Widget tree, the Runtime + reconcile boundary, and direct
# cvc::state binding, walking a pluggable Backend. It touches NO VTK/ImGui/GL —
# the ImGui-over-VTK backend lives in cvcGL (src/cvcGL/ariadne). Roadmap §16.1.
list(APPEND SOURCE_FILES ariadne/ariadne.cpp ariadne/loader.cpp ariadne/uri.cpp ariadne/uri_state.cpp ariadne/uri_http.cpp ariadne/uri_http_cache.cpp ariadne/state_io.cpp ariadne/ftxui_backend.cpp)
list(APPEND SOURCE_FILES ariadne/ariadne.cpp ariadne/loader.cpp ariadne/uri.cpp ariadne/uri_state.cpp ariadne/uri_http.cpp ariadne/uri_http_cache.cpp ariadne/net_intrinsics.cpp ariadne/state_io.cpp ariadne/ftxui_backend.cpp)
list(APPEND INCLUDE_FILES
../../inc/cvc/ariadne/widget.h
../../inc/cvc/ariadne/backend.h
Expand All @@ -1129,6 +1129,7 @@ list(APPEND INCLUDE_FILES
../../inc/cvc/ariadne/uri_state.h
../../inc/cvc/ariadne/uri_http.h
../../inc/cvc/ariadne/uri_http_cache.h
../../inc/cvc/ariadne/net_intrinsics.h
../../inc/cvc/ariadne/state_io.h
../../inc/cvc/ariadne/scene.h
../../inc/cvc/ariadne/value.h
Expand Down
128 changes: 128 additions & 0 deletions src/cvc/ariadne/net_intrinsics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Ariadne — async HTTP host intrinsics for the state_exec program lanes (roadmap §13.8). See
// net_intrinsics.h. The whole implementation is gated on CVC_STATE_EXEC (no program lanes without
// it) and on a compiled cvc::net backend (cvc::net::have_http_backend()).

#include <cvc/ariadne/ariadne.h> // register_action_intrinsics, have_state_exec
#include <cvc/ariadne/net_intrinsics.h>
#include <cvc/core/app.h>
#include <cvc/net/http_client.h>

#ifdef CVC_STATE_EXEC

#include <atomic>
#include <cstdint>
#include <cvc/core/state_exec/async_scheduler.h> // exec_scheduler().post_message
#include <cvc/core/state_exec/builtins.h> // register_fn — bind the host verb into the lanes
#include <cvc/core/state_exec/intrinsics.h> // resolve_channel_key — scope the reply channel
#include <cvc/core/state_exec/types.h> // value_t, make_dict/make_list/make_bytes
#include <span>
#include <string>
#include <utility>
#include <vector>

namespace cvc {
namespace ariadne {

namespace se = cvc::state_exec;

namespace {

// Marshal a cvc::net::HttpResponse into the DSL reply dict. EVERY key is populated on every path
// (get-attr throws on a missing key), so an error response is a well-formed dict too. The body is
// `bytes` (opaque octets, the sanctioned home for an HTTP body — the state_exec bytes track).
se::value_t marshal_response(const cvc::net::HttpResponse &r) {
std::vector<se::value_t> headers;
headers.reserve(r.headers.size());
for (const std::string &h : r.headers)
headers.push_back(se::value_t(h));
return se::make_dict({
{"ok", se::value_t(r.ok)},
{"status", se::value_t(static_cast<int64_t>(r.status))},
{"body", se::make_bytes(r.body)},
{"url", se::value_t(r.canonical_url)},
{"headers", se::make_list(std::move(headers))},
{"error", se::value_t(r.error)},
});
}

se::value_t marshal_error(const std::string &url, const std::string &message) {
cvc::net::HttpResponse r;
r.ok = false;
r.status = 0;
r.canonical_url = url;
r.error = message;
return marshal_response(r);
}

} // namespace

void register_net_intrinsics(cvc::app &app) {
if (!have_state_exec() || !cvc::net::have_http_backend())
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();

register_action_intrinsics(
[&app](std::shared_ptr<se::environment> env, se::intrinsics_context &ictx) {
// Capture the lane's chroot so the reply channel is scoped the same way the program's
// (msg-recv <chan>) will resolve it. The verb generates a UNIQUE '#'-suffixed channel per
// call (returned verbatim by resolve_channel_key + exempt from channel-policy), so two
// in-flight fetches never collide on the single recv_path a process has.
const std::string root = ictx.root_path;
se::builtins::register_fn(
env, "http-get-async", [&app, root](std::span<const se::value_t> args) -> se::value_t {
cvc::net::HttpRequest req;
if (!args.empty())
if (const std::string *url = std::get_if<std::string>(&args[0].v))
req.url = *url;
// Optional second arg: a list of verbatim "Name: value" header strings.
if (args.size() > 1)
if (const se::list_ptr *lp = std::get_if<se::list_ptr>(&args[1].v))
for (const se::value_t &e : **lp)
if (const std::string *h = std::get_if<std::string>(&e.v))
req.headers.push_back(*h);

static std::atomic<std::uint64_t> seq{0};
const std::string chan =
"http.reply#" + std::to_string(seq.fetch_add(1, std::memory_order_relaxed));
const std::string done = se::resolve_channel_key(root, chan); // '#' => == chan

// Run the BLOCKING fetch on a compute-pool worker; post the marshalled dict when it
// joins. compute_async returns immediately, so the verb never blocks the scheduler
// thread. The worker ALWAYS posts (an error dict on a throw), so the parked
// (msg-recv) always resumes.
const std::string url = req.url;
app.compute_async(
1, [](int) {},
[&app, req, done, url] {
se::value_t payload;
try {
payload = marshal_response(cvc::net::send(req));
} catch (const std::exception &e) {
payload = marshal_error(url, std::string("http-get: ") + e.what());
} catch (...) {
payload = marshal_error(url, "http-get: unknown error");
}
app.exec_scheduler().post_message(done, payload);
});
return se::value_t(chan); // the program awaits with (msg-recv <chan>)
});
});
}

} // namespace ariadne
} // namespace cvc

#else // !CVC_STATE_EXEC

namespace cvc {
namespace ariadne {
void register_net_intrinsics(cvc::app & /*app*/) {
// No program lanes without state_exec — nothing to register.
}
} // namespace ariadne
} // namespace cvc

#endif // CVC_STATE_EXEC
Loading
Loading