feat: add await syntax for direct async function calls and new generator/phonebook apps

Add compiler support for calling async functions directly with `await fn(args)` syntax, automatically translating to `await fn.call(args)`. Introduce `create_merge` Makefile target for merging Wren source files. Include new `apps/generator.wren` source code generator using OpenRouter API, `apps/phonebook.wren` CLI phonebook with SQLite backend, and `apps/minitut.md` Wren tutorial reference. Update `example/await_demo.wren` with parameterized async functions, nested awaits, and expression usage. Fix JSON parsing in `example/openrouter_demo.wren` to strip markdown code fences. Document new await syntax in `manual/api/scheduler.html`.
This commit is contained in:
2026-01-25 09:50:20 +00:00
parent e0cc7791e3
commit eadfc8e9cb
48 changed files with 1543 additions and 86 deletions
+14 -10
View File
@@ -137,18 +137,21 @@ class Http {
requestHeaders["User-Agent"] = "Wren-CLI/1.0"
if (bodyStr.count > 0) {
requestHeaders["Content-Length"] = bodyStr.count.toString
requestHeaders["Content-Length"] = bodyStr.bytes.count.toString
}
for (entry in headers) {
requestHeaders[entry.key] = entry.value
}
var request = "%(method) %(parsed.fullPath) HTTP/1.1\r\n"
var parts = []
parts.add("%(method) %(parsed.fullPath) HTTP/1.1\r\n")
for (entry in requestHeaders) {
request = request + "%(entry.key): %(entry.value)\r\n"
parts.add("%(entry.key): %(entry.value)\r\n")
}
request = request + "\r\n" + bodyStr
parts.add("\r\n")
parts.add(bodyStr)
var request = parts.join("")
var addresses = Dns.lookup(parsed.host, 4)
if (addresses.count == 0) {
@@ -163,12 +166,13 @@ class Http {
}
socket.write(request)
var response = ""
var chunks = []
while (true) {
var chunk = socket.read()
if (chunk == null || chunk.count == 0) break
response = response + chunk
if (chunk == null || chunk.bytes.count == 0) break
chunks.add(chunk)
}
var response = chunks.join("")
socket.close()
return parseResponse_(response)
@@ -251,7 +255,7 @@ class Http {
}
static decodeChunked_(body) {
var result = ""
var parts = []
var pos = 0
while (pos < body.count) {
@@ -266,11 +270,11 @@ class Http {
pos = lineEnd + 2
if (pos + size > body.count) break
result = result + body[pos...(pos + size)]
parts.add(body[pos...(pos + size)])
pos = pos + size + 2
}
return result
return parts.join("")
}
static parseHex_(str) {
+14 -10
View File
@@ -141,18 +141,21 @@ static const char* httpModuleSource =
" requestHeaders[\"User-Agent\"] = \"Wren-CLI/1.0\"\n"
"\n"
" if (bodyStr.count > 0) {\n"
" requestHeaders[\"Content-Length\"] = bodyStr.count.toString\n"
" requestHeaders[\"Content-Length\"] = bodyStr.bytes.count.toString\n"
" }\n"
"\n"
" for (entry in headers) {\n"
" requestHeaders[entry.key] = entry.value\n"
" }\n"
"\n"
" var request = \"%(method) %(parsed.fullPath) HTTP/1.1\\r\\n\"\n"
" var parts = []\n"
" parts.add(\"%(method) %(parsed.fullPath) HTTP/1.1\\r\\n\")\n"
" for (entry in requestHeaders) {\n"
" request = request + \"%(entry.key): %(entry.value)\\r\\n\"\n"
" parts.add(\"%(entry.key): %(entry.value)\\r\\n\")\n"
" }\n"
" request = request + \"\\r\\n\" + bodyStr\n"
" parts.add(\"\\r\\n\")\n"
" parts.add(bodyStr)\n"
" var request = parts.join(\"\")\n"
"\n"
" var addresses = Dns.lookup(parsed.host, 4)\n"
" if (addresses.count == 0) {\n"
@@ -167,12 +170,13 @@ static const char* httpModuleSource =
" }\n"
" socket.write(request)\n"
"\n"
" var response = \"\"\n"
" var chunks = []\n"
" while (true) {\n"
" var chunk = socket.read()\n"
" if (chunk == null || chunk.count == 0) break\n"
" response = response + chunk\n"
" if (chunk == null || chunk.bytes.count == 0) break\n"
" chunks.add(chunk)\n"
" }\n"
" var response = chunks.join(\"\")\n"
" socket.close()\n"
"\n"
" return parseResponse_(response)\n"
@@ -255,7 +259,7 @@ static const char* httpModuleSource =
" }\n"
"\n"
" static decodeChunked_(body) {\n"
" var result = \"\"\n"
" var parts = []\n"
" var pos = 0\n"
"\n"
" while (pos < body.count) {\n"
@@ -270,11 +274,11 @@ static const char* httpModuleSource =
" pos = lineEnd + 2\n"
" if (pos + size > body.count) break\n"
"\n"
" result = result + body[pos...(pos + size)]\n"
" parts.add(body[pos...(pos + size)])\n"
" pos = pos + size + 2\n"
" }\n"
"\n"
" return result\n"
" return parts.join(\"\")\n"
" }\n"
"\n"
" static parseHex_(str) {\n"
+84 -22
View File
@@ -31,6 +31,9 @@ static WrenHandle* stdinOnData = NULL;
// The stream used to read from stdin. Initialized on the first read.
static uv_stream_t* stdinStream = NULL;
// True if stdin is a file/device that doesn't support async I/O.
static bool stdinIsFile = false;
// True if stdin has been set to raw mode.
static bool isStdinRaw = false;
@@ -514,11 +517,19 @@ static void initStdin()
}
else
{
// stdin is a pipe or a file.
uv_pipe_t* handle = (uv_pipe_t*)malloc(sizeof(uv_pipe_t));
uv_pipe_init(getLoop(), handle, false);
uv_pipe_open(handle, stdinDescriptor);
stdinStream = (uv_stream_t*)handle;
uv_handle_type htype = uv_guess_handle(stdinDescriptor);
if (htype == UV_FILE || htype == UV_UNKNOWN_HANDLE)
{
stdinIsFile = true;
stdinStream = NULL;
}
else
{
uv_pipe_t* handle = (uv_pipe_t*)malloc(sizeof(uv_pipe_t));
uv_pipe_init(getLoop(), handle, false);
uv_pipe_open(handle, stdinDescriptor);
stdinStream = (uv_stream_t*)handle;
}
}
}
}
@@ -577,53 +588,104 @@ static void stdinReadCallback(uv_stream_t* stream, ssize_t numRead,
const uv_buf_t* buffer)
{
WrenVM* vm = getVM();
if (stdinClass == NULL)
{
wrenEnsureSlots(vm, 1);
wrenGetVariable(vm, "io", "Stdin", 0);
stdinClass = wrenGetSlotHandle(vm, 0);
}
if (stdinOnData == NULL)
{
stdinOnData = wrenMakeCallHandle(vm, "onData_(_)");
}
// If stdin was closed, send null to let io.wren know.
if (numRead == UV_EOF)
if (numRead < 0)
{
if (numRead != UV_EOF)
{
uv_read_stop(stdinStream);
uv_stop(getLoop());
setExitCode(70);
return;
}
wrenEnsureSlots(vm, 2);
wrenSetSlotHandle(vm, 0, stdinClass);
wrenSetSlotNull(vm, 1);
wrenCall(vm, stdinOnData);
shutdownStdin();
WrenInterpretResult result = wrenCall(vm, stdinOnData);
if (result == WREN_RESULT_RUNTIME_ERROR)
{
uv_stop(getLoop());
setExitCode(70);
}
return;
}
// TODO: Handle other errors.
// TODO: Having to copy the bytes here is a drag. It would be good if Wren's
// embedding API supported a way to *give* it bytes that were previously
// allocated using Wren's own allocator.
wrenEnsureSlots(vm, 2);
wrenSetSlotHandle(vm, 0, stdinClass);
wrenSetSlotBytes(vm, 1, buffer->base, numRead);
wrenCall(vm, stdinOnData);
WrenInterpretResult result = wrenCall(vm, stdinOnData);
// TODO: Likewise, freeing this after we resume is lame.
free(buffer->base);
if (result == WREN_RESULT_RUNTIME_ERROR)
{
uv_stop(getLoop());
setExitCode(70);
}
}
static uv_timer_t stdinFileTimer;
static void stdinFileTimerCallback(uv_timer_t* handle)
{
WrenVM* vm = getVM();
if (stdinClass == NULL)
{
wrenEnsureSlots(vm, 1);
wrenGetVariable(vm, "io", "Stdin", 0);
stdinClass = wrenGetSlotHandle(vm, 0);
}
if (stdinOnData == NULL)
{
stdinOnData = wrenMakeCallHandle(vm, "onData_(_)");
}
wrenEnsureSlots(vm, 2);
wrenSetSlotHandle(vm, 0, stdinClass);
wrenSetSlotNull(vm, 1);
WrenInterpretResult result = wrenCall(vm, stdinOnData);
if (result == WREN_RESULT_RUNTIME_ERROR)
{
uv_stop(getLoop());
setExitCode(70);
}
}
void stdinReadStart(WrenVM* vm)
{
initStdin();
if (stdinIsFile)
{
uv_timer_init(getLoop(), &stdinFileTimer);
uv_timer_start(&stdinFileTimer, stdinFileTimerCallback, 0, 0);
return;
}
uv_read_start(stdinStream, allocCallback, stdinReadCallback);
// TODO: Check return.
}
void stdinReadStop(WrenVM* vm)
{
uv_read_stop(stdinStream);
if (stdinStream != NULL)
{
uv_read_stop(stdinStream);
}
}
+1
View File
@@ -280,6 +280,7 @@ class Stdin {
} else {
__waitingFiber.transferError("Stdin was closed.")
}
return
}
// Append to the buffer.
+1
View File
@@ -284,6 +284,7 @@ static const char* ioModuleSource =
" } else {\n"
" __waitingFiber.transferError(\"Stdin was closed.\")\n"
" }\n"
" return\n"
" }\n"
"\n"
" // Append to the buffer.\n"
+10 -2
View File
@@ -1,6 +1,7 @@
import "meta" for Meta
import "io" for Stdin, Stdout
import "os" for Platform
import "scheduler" for Scheduler
/// Abstract base class for the REPL. Manages the input line and history, but
/// does not render.
@@ -65,7 +66,7 @@ class Repl {
} else {
// TODO: Handle ESC 0 sequences.
}
} else if (byte == Chars.carriageReturn) {
} else if (byte == Chars.carriageReturn || byte == Chars.lineFeed) {
executeInput()
} else if (byte == Chars.delete) {
deleteLeft()
@@ -197,8 +198,15 @@ class Repl {
var fiber = Fiber.new(closure)
var result = fiber.try()
while (!fiber.isDone && fiber.error == null && Scheduler.hasScheduled_) {
Scheduler.runNextScheduled_()
if (!fiber.isDone && fiber.error == null) {
result = fiber.try()
}
}
if (fiber.error != null) {
// TODO: Include callstack.
showRuntimeError("Runtime error: %(fiber.error)")
return
}
+10 -2
View File
@@ -5,6 +5,7 @@ static const char* replModuleSource =
"import \"meta\" for Meta\n"
"import \"io\" for Stdin, Stdout\n"
"import \"os\" for Platform\n"
"import \"scheduler\" for Scheduler\n"
"\n"
"/// Abstract base class for the REPL. Manages the input line and history, but\n"
"/// does not render.\n"
@@ -69,7 +70,7 @@ static const char* replModuleSource =
" } else {\n"
" // TODO: Handle ESC 0 sequences.\n"
" }\n"
" } else if (byte == Chars.carriageReturn) {\n"
" } else if (byte == Chars.carriageReturn || byte == Chars.lineFeed) {\n"
" executeInput()\n"
" } else if (byte == Chars.delete) {\n"
" deleteLeft()\n"
@@ -201,8 +202,15 @@ static const char* replModuleSource =
" var fiber = Fiber.new(closure)\n"
"\n"
" var result = fiber.try()\n"
"\n"
" while (!fiber.isDone && fiber.error == null && Scheduler.hasScheduled_) {\n"
" Scheduler.runNextScheduled_()\n"
" if (!fiber.isDone && fiber.error == null) {\n"
" result = fiber.try()\n"
" }\n"
" }\n"
"\n"
" if (fiber.error != null) {\n"
" // TODO: Include callstack.\n"
" showRuntimeError(\"Runtime error: %(fiber.error)\")\n"
" return\n"
" }\n"
+6
View File
@@ -40,6 +40,10 @@ class Scheduler {
}
}
static hasScheduled_ {
return __scheduled != null && !__scheduled.isEmpty
}
foreign static captureMethods_()
}
@@ -71,6 +75,8 @@ class Future {
isDone { _resolved }
result { _result }
call() { this }
}
Scheduler.captureMethods_()
+6
View File
@@ -44,6 +44,10 @@ static const char* schedulerModuleSource =
" }\n"
" }\n"
"\n"
" static hasScheduled_ {\n"
" return __scheduled != null && !__scheduled.isEmpty\n"
" }\n"
"\n"
" foreign static captureMethods_()\n"
"}\n"
"\n"
@@ -75,6 +79,8 @@ static const char* schedulerModuleSource =
"\n"
" isDone { _resolved }\n"
" result { _result }\n"
"\n"
" call() { this }\n"
"}\n"
"\n"
"Scheduler.captureMethods_()\n";
+26 -8
View File
@@ -336,14 +336,32 @@ void tlsSocketWrite(WrenVM* vm) {
data->fiber = fiber;
data->writeInProgress = true;
int written = SSL_write(data->ssl, text, length);
if (written <= 0) {
int err = SSL_get_error(data->ssl, written);
if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
data->writeInProgress = false;
data->fiber = NULL;
schedulerResumeError(fiber, "SSL write error");
return;
int totalWritten = 0;
while (totalWritten < length) {
int written = SSL_write(data->ssl, text + totalWritten, length - totalWritten);
if (written > 0) {
totalWritten += written;
} else {
int err = SSL_get_error(data->ssl, written);
if (err == SSL_ERROR_WANT_WRITE) {
char buffer[16384];
int pending;
while ((pending = BIO_read(data->writeBio, buffer, sizeof(buffer))) > 0) {
TlsWriteRequest* wr = (TlsWriteRequest*)malloc(sizeof(TlsWriteRequest));
wr->buf.base = (char*)malloc(pending);
wr->buf.len = pending;
memcpy(wr->buf.base, buffer, pending);
wr->data = data;
wr->isLast = false;
wr->req.data = wr;
uv_write(&wr->req, (uv_stream_t*)data->handle, &wr->buf, 1, tlsWriteCallback);
}
} else if (err != SSL_ERROR_WANT_READ) {
data->writeInProgress = false;
data->fiber = NULL;
schedulerResumeError(fiber, "SSL write error");
return;
}
}
}
+2 -2
View File
@@ -431,7 +431,7 @@ class Application {
while (true) {
var chunk = socket.read()
if (chunk == null || chunk.count == 0) {
if (chunk == null || chunk.bytes.count == 0) {
socket.close()
return
}
@@ -637,7 +637,7 @@ class Client {
var response = ""
while (true) {
var chunk = socket.read()
if (chunk == null || chunk.count == 0) break
if (chunk == null || chunk.bytes.count == 0) break
response = response + chunk
}
socket.close()
+2 -2
View File
@@ -435,7 +435,7 @@ static const char* webModuleSource =
"\n"
" while (true) {\n"
" var chunk = socket.read()\n"
" if (chunk == null || chunk.count == 0) {\n"
" if (chunk == null || chunk.bytes.count == 0) {\n"
" socket.close()\n"
" return\n"
" }\n"
@@ -641,7 +641,7 @@ static const char* webModuleSource =
" var response = \"\"\n"
" while (true) {\n"
" var chunk = socket.read()\n"
" if (chunk == null || chunk.count == 0) break\n"
" if (chunk == null || chunk.bytes.count == 0) break\n"
" response = response + chunk\n"
" }\n"
" socket.close()\n"
+3 -3
View File
@@ -112,7 +112,7 @@ class WebSocket {
var response = ""
while (true) {
var chunk = socket.read()
if (chunk == null || chunk.count == 0) {
if (chunk == null || chunk.bytes.count == 0) {
Fiber.abort("Connection closed during handshake")
}
response = response + chunk
@@ -428,7 +428,7 @@ class WebSocket {
readBytes_(count) {
while (Bytes.length(_readBuffer) < count) {
var chunk = _socket.read()
if (chunk == null || chunk.count == 0) {
if (chunk == null || chunk.bytes.count == 0) {
if (Bytes.length(_readBuffer) == 0) return null
var result = _readBuffer
_readBuffer = ""
@@ -469,7 +469,7 @@ class WebSocketServer {
var request = ""
while (true) {
var chunk = socket.read()
if (chunk == null || chunk.count == 0) {
if (chunk == null || chunk.bytes.count == 0) {
socket.close()
return null
}
+3 -3
View File
@@ -116,7 +116,7 @@ static const char* websocketModuleSource =
" var response = \"\"\n"
" while (true) {\n"
" var chunk = socket.read()\n"
" if (chunk == null || chunk.count == 0) {\n"
" if (chunk == null || chunk.bytes.count == 0) {\n"
" Fiber.abort(\"Connection closed during handshake\")\n"
" }\n"
" response = response + chunk\n"
@@ -432,7 +432,7 @@ static const char* websocketModuleSource =
" readBytes_(count) {\n"
" while (Bytes.length(_readBuffer) < count) {\n"
" var chunk = _socket.read()\n"
" if (chunk == null || chunk.count == 0) {\n"
" if (chunk == null || chunk.bytes.count == 0) {\n"
" if (Bytes.length(_readBuffer) == 0) return null\n"
" var result = _readBuffer\n"
" _readBuffer = \"\"\n"
@@ -473,7 +473,7 @@ static const char* websocketModuleSource =
" var request = \"\"\n"
" while (true) {\n"
" var chunk = socket.read()\n"
" if (chunk == null || chunk.count == 0) {\n"
" if (chunk == null || chunk.bytes.count == 0) {\n"
" socket.close()\n"
" return null\n"
" }\n"