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:
Vendored
+14
-10
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -280,6 +280,7 @@ class Stdin {
|
||||
} else {
|
||||
__waitingFiber.transferError("Stdin was closed.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Append to the buffer.
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+10
-2
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+6
@@ -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_()
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Vendored
+3
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user