Add async/await, non-blocking I/O, and networking stdlib#3
Conversation
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
|
@codex review |
There was a problem hiding this comment.
💡 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".
| TaskResolution::Ready(value) => { | ||
| self.push(value); | ||
| self.await_task_roots.pop(); |
There was a problem hiding this comment.
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 👍 / 👎.
| if *self.user_channels[id].closed.borrow() { | ||
| return Ok(false); | ||
| } |
There was a problem hiding this comment.
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 👍 / 👎.
|
@codex address the feedback |
Summary
Testing
|
- 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.
|
@codex review PR |
There was a problem hiding this comment.
💡 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".
| if body_bytes.len() > parsed_head.content_length { | ||
| body_bytes.truncate(parsed_head.content_length); |
There was a problem hiding this comment.
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 👍 / 👎.
| } | ||
|
|
||
| pub(crate) fn run_queued_tasks(&mut self, span: Span) -> WalrusResult<()> { | ||
| while let Some(task_key) = self.next_runnable_task()? { |
There was a problem hiding this comment.
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 👍 / 👎.
Add async/await, non-blocking I/O, and networking stdlib
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
async fn/awaitsyntax with full closure capture supportasyncx.spawn(fn, args)— schedule tasks for concurrent executionasyncx.gather(tasks)— wait for all tasks (short-circuits on first failure, likePromise.all)asyncx.race(tasks)— return first completed result (likePromise.race)asyncx.all_settled(tasks)— wait for all tasks regardless of success/failure, returns[{status, value/error}](likePromise.allSettled)asyncx.sleep(ms)/asyncx.timeout(task, ms)— timers and deadlinesasyncx.cancel(task)/asyncx.status(task)— task lifecycle managementasyncx.yield()— cooperative yielding for round-robin schedulingasyncx.channel()/asyncx.send()/asyncx.recv()— Go-style channels for task-to-task communicationNon-blocking I/O
Networking & HTTP (
std/net,std/http)tcp_bind,tcp_accept,tcp_connect,tcp_read,tcp_write,tcp_closeread_request,response,response_with_headers,match_route/users/:id,/users/:id/posts/:post_id,/assets/*wildcardStandard Library Additions
std/json—json.encode()/json.decode()powered by serde_jsoncore.timestamp()— epoch millisecondsLanguage Improvements
self.handler(req)works whenhandleris a function stored in a fieldHTTP Server Example (
examples/http_router_sim/)A real concurrent HTTP server written entirely in Walrus demonstrating Express-style routing:
Features: middleware, route params, wildcards, custom 404/error handlers, keep-alive, concurrent connection handling via async tasks.