Skip to content

Add async/await, non-blocking I/O, and networking stdlib#3

Merged
sb2bg merged 30 commits into
masterfrom
async-await-vm
Mar 8, 2026
Merged

Add async/await, non-blocking I/O, and networking stdlib#3
sb2bg merged 30 commits into
masterfrom
async-await-vm

Conversation

@sb2bg
Copy link
Copy Markdown
Owner

@sb2bg sb2bg commented Mar 5, 2026

Summary

Adds a complete async/await system to Walrus with non-blocking I/O, networking primitives, and a concurrent HTTP server example, bringing the language to near-JavaScript/Python level for async programming.

Async Runtime

  • Single-threaded event loop with cooperative multitasking — no race conditions, simple mental model
  • async fn / await syntax with full closure capture support
  • asyncx.spawn(fn, args) — schedule tasks for concurrent execution
  • asyncx.gather(tasks) — wait for all tasks (short-circuits on first failure, like Promise.all)
  • asyncx.race(tasks) — return first completed result (like Promise.race)
  • asyncx.all_settled(tasks) — wait for all tasks regardless of success/failure, returns [{status, value/error}] (like Promise.allSettled)
  • asyncx.sleep(ms) / asyncx.timeout(task, ms) — timers and deadlines
  • asyncx.cancel(task) / asyncx.status(task) — task lifecycle management
  • asyncx.yield() — cooperative yielding for round-robin scheduling
  • asyncx.channel() / asyncx.send() / asyncx.recv() — Go-style channels for task-to-task communication

Non-blocking I/O

  • Background worker threads for blocking syscalls (TCP accept, read, write)
  • Wakeup channel for efficient event loop notification on I/O completion
  • VM remains single-threaded — worker threads only handle I/O, never touch VM state

Networking & HTTP (std/net, std/http)

  • TCP primitives: tcp_bind, tcp_accept, tcp_connect, tcp_read, tcp_write, tcp_close
  • HTTP parsing: read_request, response, response_with_headers, match_route
  • Route matching: /users/:id, /users/:id/posts/:post_id, /assets/* wildcard

Standard Library Additions

  • std/jsonjson.encode() / json.decode() powered by serde_json
  • core.timestamp() — epoch milliseconds

Language Improvements

  • Struct fields as callablesself.handler(req) works when handler is a function stored in a field
  • try/catch no longer requires trailing semicolon
  • dict.get(key, default) — safe access with fallback for missing keys

HTTP Server Example (examples/http_router_sim/)

A real concurrent HTTP server written entirely in Walrus demonstrating Express-style routing:

app.get("/users/:id", : req {
    let id = req["params"]["id"];
    return router.json(200, json.encode({"id": id}));
});

app.get("/slow", : req {
    await asyncx.sleep(1000);  // doesn't block other requests
    return router.text(200, "done\n");
});

app.listen("127.0.0.1", 8080);

Features: middleware, route params, wildcards, custom 404/error handlers, keep-alive, concurrent connection handling via async tasks.

sb2bg and others added 15 commits March 4, 2026 18:00
Support `async : args { body }` syntax for anonymous async functions,
mirroring the existing lambda syntax with an `async` prefix.
race(tasks) resolves to the first completed task result, skipping
failures unless all tasks fail. status(task) returns the task state
as a string: pending, ready, failed, or cancelled.
Organize flat pass/fail fixture directories into semantic categories
(async, basics, collections, control_flow, functions, modules, stdlib,
strings) and update the test runner to recursively discover fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yield voluntarily gives other queued tasks a turn to run, then resumes.
Works as a no-op when no tasks are queued. Enables round-robin style
interleaving between concurrent async tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yield is a scheduling hint, not a language construct — it belongs in
std/async alongside spawn, sleep, cancel, etc. Removes Yield from AST,
grammar, and opcode; adds AsyncYield native function with vm.run_queued_tasks().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Node.js-style architecture: blocking TCP and HTTP operations now run on
worker threads, communicating results back to the single-threaded VM via
mpsc channels. The event loop waits on a wakeup channel instead of
sleeping, so it resumes immediately when I/O completes.

Async operations: tcp_accept, tcp_connect, tcp_read, tcp_read_line,
tcp_write, and http.read_request. Sync operations unchanged: tcp_bind,
tcp_close, tcp_local_port, and HTTP response builders.
…ess-style HTTP server

- Add std/json module with encode() and decode() (recursive descent parser)
- Allow struct fields holding functions to be called via method syntax
- Make try/catch trailing semicolon optional in grammar
- Rewrite http_router_sim as full Express-style framework with middleware,
  route params, wildcards, error handling, keep-alive, and concurrent connections
- Fix dict safe access in router (use .get() with default for missing headers)
- Add regressions for all changes: json encode/decode, struct field callable,
  try/catch no semicolon, core.timestamp(), dict.get() default
- all_settled: like gather but never short-circuits — collects
  {status: "ok"/"error"/"cancelled", value/error} for every task
- Channels: asyncx.channel() returns [sender, receiver] for
  task-to-task communication via asyncx.send() and asyncx.recv()
- Add regressions for both features
@sb2bg sb2bg requested a review from Copilot March 5, 2026 17:38
@sb2bg
Copy link
Copy Markdown
Owner Author

sb2bg commented Mar 5, 2026

@codex review

Repository owner deleted a comment from chatgpt-codex-connector Bot Mar 5, 2026
@sb2bg sb2bg review requested due to automatic review settings March 5, 2026 17:44
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 254a5c72eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/vm/mod.rs Outdated
Comment on lines +3506 to +3508
TaskResolution::Ready(value) => {
self.push(value);
self.await_task_roots.pop();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid pushing awaited result twice onto operand stack

When an awaited task is a runnable Pending function, run_pending_task_to_completion executes it and the callee Return path already pushes its return value onto the caller stack; this Await branch then pushes the same value again. That duplicates stack operands, so expressions can compute with the wrong values (for example 5 + await f() can consume two copies of f()), and repeated await statements leak extra stack entries in long-lived frames.

Useful? React with 👍 / 👎.

Comment thread src/vm/mod.rs Outdated
Comment on lines +925 to +927
if *self.user_channels[id].closed.borrow() {
return Ok(false);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make channel closed checks reachable

channel_send gates on user_channels[id].closed, but this flag is initialized to false and never set to true anywhere in the VM, so the closed path is effectively unreachable. As a result, asyncx.send cannot report a closed receiver and asyncx.recv waiters can remain pending forever once producers stop, leading to avoidable deadlocks in channel-based workflows.

Useful? React with 👍 / 👎.

Repository owner deleted a comment from chatgpt-codex-connector Bot Mar 5, 2026
Repository owner deleted a comment from chatgpt-codex-connector Bot Mar 6, 2026
@sb2bg
Copy link
Copy Markdown
Owner Author

sb2bg commented Mar 6, 2026

@codex address the feedback

@chatgpt-codex-connector
Copy link
Copy Markdown

Summary

Testing

  • cargo fmt
  • cargo test -q

View task →

sb2bg added 4 commits March 6, 2026 03:11
- Deleted execution mode files for various tests, simplifying the test suite.
- Removed ExecMode enum and associated logic from language suite tests.
- Updated test cases to run without specifying execution modes.
@sb2bg
Copy link
Copy Markdown
Owner Author

sb2bg commented Mar 8, 2026

@codex review PR

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c32c4d154a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/stdlib/http.rs Outdated
Comment on lines +358 to +359
if body_bytes.len() > parsed_head.content_length {
body_bytes.truncate(parsed_head.content_length);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve unread bytes when parsing HTTP request bodies

The request reader drops any bytes already read beyond the current body's Content-Length: it copies the post-header tail into body_bytes and then truncates it. When a keep-alive client sends requests back-to-back, read() can return data that includes the next request, and this truncation silently discards that next request data, causing corrupted/lost follow-up requests on the same connection.

Useful? React with 👍 / 👎.

Comment thread src/vm/mod.rs Outdated
}

pub(crate) fn run_queued_tasks(&mut self, span: Span) -> WalrusResult<()> {
while let Some(task_key) = self.next_runnable_task()? {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh waiters even when no runnable task exists

run_queued_tasks only refreshes waiter state after executing a runnable task, so if the queue is empty it exits immediately without polling suspended dependencies. This misses cases where a task became ready due side effects outside the runnable queue (for example asyncx.send from currently running main code making a UserRecv task ready), leaving waiting tasks suspended and making asyncx.yield() fail to advance them.

Useful? React with 👍 / 👎.

@sb2bg sb2bg merged commit 73ea8eb into master Mar 8, 2026
4 checks passed
@sb2bg sb2bg deleted the async-await-vm branch March 8, 2026 01:46
sb2bg added a commit that referenced this pull request Mar 8, 2026
Add async/await, non-blocking I/O, and networking stdlib
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant