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
Vendored Executable
+146
View File
@@ -0,0 +1,146 @@
#!/usr/local/bin/wren
import "pathlib" for Path
import "http" for Http
import "json" for Json
import "env" for Environment
import "argparse" for ArgumentParser
import "os" for Process
class SourceCodeGenerator {
static VERSION { "1.0" }
static DEFAULT_MODEL { "anthropic/claude-haiku-4.5" }
static API_ENDPOINT { "https://openrouter.ai/api/v1/chat/completions" }
static TUTORIAL_PATH { "apps/minitut.md" }
static DEFAULT_PROMPT {
"A small but useful application that combines several modules / libraries that is plug and play to use! Small description on top commenten what the application is and what it does and where it can be used for."
}
static run() {
System.print("// wren source code generator v%(this.VERSION)")
var startTime = System.clock
var args = parseArguments()
var apiKey = getApiKey()
var tutorial = loadTutorial()
var messages = buildMessages(tutorial, args["prompt"])
var sourceCode = generateCode(apiKey, messages)
printResults(startTime, sourceCode)
saveOutput(args["output"], sourceCode)
}
static parseArguments() {
var parser = ArgumentParser.new()
parser.addArgument("prompt", {"default": this.DEFAULT_PROMPT})
parser.addArgument("-o", {"long": "--output", "default": ""})
return parser.parseArgs()
}
static getApiKey() {
var apiKey = Environment.get("OPENROUTER_API_KEY")
if (apiKey == null || apiKey.count == 0) {
System.print("Error: OPENROUTER_API_KEY environment variable not set")
Fiber.abort("Missing API key")
}
return apiKey
}
static loadTutorial() {
return Path.new(this.TUTORIAL_PATH).readText()
}
static buildMessages(tutorial, userPrompt) {
var messages = []
messages.add({
"role": "system",
"content": "You are an application generator that will produce wren source code exclusively as described below:\n" + tutorial
})
messages.add({
"role": "user",
"content": "Please generate source code based on everything what i say from now on."
})
messages.add({
"role": "assistant",
"content": "I will respond in literally valid JSON only in this format: {\"source_code\":\"<the source code>\"} without any markup or markdown formatting."
})
messages.add({
"role": "user",
"content": userPrompt
})
return messages
}
static generateCode(apiKey, messages) {
var requestBody = {
"model": "%(this.DEFAULT_MODEL)",
"messages": messages
}
var headers = {
"Authorization": "Bearer " + apiKey,
"Content-Type": "application/json"
}
var response = Http.post(this.API_ENDPOINT, requestBody, headers)
if (!response.ok) {
System.print("Error: %(response.body)")
return ""
}
return extractSourceCode(response.body)
}
static extractSourceCode(responseBody) {
var data = Json.parse(responseBody)
if (data == null || data["choices"] == null || data["choices"].count == 0) {
return ""
}
var message = data["choices"][0]["message"]
if (message == null || message["content"] == null) {
return ""
}
var text = message["content"]
text = stripMarkdownWrapper(text)
text = extractFromJson(text)
return text
}
static stripMarkdownWrapper(text) {
if (text.startsWith("```")) {
return text[7..-4].trim()
}
return text
}
static extractFromJson(text) {
if (text.startsWith("{")) {
return Json.parse(text)["source_code"]
}
return text
}
static printResults(startTime, output) {
var elapsedTime = System.clock - startTime
System.print("// Generation time: %(elapsedTime)")
System.print("")
System.print(output)
}
static saveOutput(outputPath, content) {
if (!outputPath.isEmpty) {
Path.new(outputPath).writeText(content)
}
}
}
System.print(SourceCodeGenerator.DEFAULT_MODEL)
SourceCodeGenerator.run()
+107
View File
@@ -0,0 +1,107 @@
# Wren Tutorial: Core to Advanced
## Basics
Wren: OOP scripting. Classes: `class Name { construct new(args) { _vars } methods }`. Inherit: `is Super`. Fn: `Fn.new { |args| body }`. Import: `import "mod" for Items`. Vars: `var x = val`. Loops: `for (i in seq) { }`, `while (cond) { }`. Conditionals: `if/else`. Errors: `Fiber.abort("msg")`.
Example:
```
class Point {
construct new(x, y) { _x = x; _y = y }
x { _x }
}
var p = Point.new(1, 2)
System.print(p.x) // 1
```
## Core Classes
- **Bool/Fiber/Fn/Null/Num**: Base.
- **Sequence**: `all/any(f)`, `contains(el)`, `count/f`, `each(f)`, `isEmpty`, `map(f)`, `skip/take(n)`, `where(f)`, `reduce/acc+f`, `join/sep`, `toList`.
- **String**: `bytes/codePoints` (seqs), `split(delim)`, `replace(from/to)`, `trim/chars/start/end`, `*(n)`.
- **List**: `addAll(other)`, `sort/comparer` (quicksort), `toString`, `+/ *(other/n)`.
- **Map**: `keys/values` (seqs), `toString`, `iteratorValue``MapEntry(key/val)`.
- **Range**: Seq empty.
- **System**: `print/obj/All`, `write/obj/All`, `clock`.
## Meta/Random
- **Meta**: `getModuleVariables(mod)`, `eval(src)`, `compileExpression/src`.
- **Random**: `new/seed`, `float/int/range`, `sample(list/n)`, `shuffle(list)`.
## IO/OS
- **File**: `read/write(path)`, `exists/delete`.
- **Directory**: `list/exists/mkdir/rmdir/delete(path)`.
- **Stdin/Stdout**: `readLine`, `flush`.
- **Process**: `args`, timed exec.
## Fibers/Scheduler/Timer
- Fiber: `new { body }`, `call/try`.
- Scheduler: `add { body }` (runs on IO).
- Timer: `sleep(ms)` (suspends).
## Modules/Examples
- **Argparse**: `new(desc)`, `addArgument(opt, {help/default/action/type})`, `parseArgs(args) → map`.
- **Base64**: `encode/decode(str)`, `encodeUrl/decodeUrl`.
- **Bytes**: `fromList/toList`, `length/concat/slice`, `xorMask(payload/mask)`.
- **Crypto**: `randomBytes(n)/Int(min,max)`; Hash: `md5/sha1/sha256(str)`, `toHex`.
- **Dataset**: `memory()["table"]`: `insert(map) → uid`, `all/find(query)/findOne`, `update/delete(uid)`, `columns/tables`.
- **Datetime**: `now/fromTimestamp`, components, `format(fmt)`, `+/- Duration` (fromHours/etc, seconds/+/-/*).
- **Dns**: `lookup(host,4/6) → [ips]`.
- **Pathlib**: `new(path)`, `exists/isDir/File`, `expanduser/glob/rglob`, `iterdir/joinpath/match/mkdir`, `name/stem/suffix/es`, `parent/parents/parts`, `readText/writeText/unlink`, `relativeTo/rename/walk`, `withName/Stem/Suffix`.
- **Regex**: `new(pat/flags)`, `test/replace/All/split`.
- **Signal**: Constants (SIGINT=2, etc.).
- **Sqlite**: `memory()`: `execute(sql,params)`, `lastInsertId/changes`, `query → rows`.
- **String Utils**: `toLower/Upper/hexEncode/Decode/repeat/padLeft/Right/escapeHtml/Json/urlEncode/Decode`.
- **Strutil**: As above.
- **Subprocess**: `run(cmd/args) → ProcessResult(success/exitCode/stdout)`.
- **Tempfile**: `gettempdir/mkdtemp/stemp/temp(suffix/prefix/dir)`, NamedTemporaryFile/TemporaryDirectory (name/write/read/close/delete/use/cleanup).
- **Uuid**: `v4() → str`, `isValid/V4`.
- **Wdantic**: Schema: `new({field: Field.type(opts)})`, `validate(data) → ValidationResult(isValid/errors/data)`; Validators: email/domain/etc.
- **Web**: Client: `parseUrl_(url) → {scheme/host/port/path}`; Request: `new_(method/path/query/headers/body/params/files)`, `header/json/cookies/form`; Response: `text/html/json/redirect/new(status/body/header/cookie/build)`; Router: `new()`, `get/post/etc(path,handler)`, `match(method/path) → {handler/params}`; SessionStore: `create/get/save/destroy`.
- **Websocket**: `computeAcceptKey(base64)`, Message: `new_(opcode/payload/fin)`, `isText/etc`; Server: `bind/accept/receive/sendBinary/close`.
## Tests/Benchmarks
- Fib: Recursive fib(28).
- Nbody: Solar system sim (bodies, energy, advance).
- Demos: Animals game (tree nodes), chat server (TCP fibers), browser auto (WS commands), etc.
## Usage
Import modules, use classes/fns. Run: Compile/eval via Meta. Async: Fibers + Scheduler/Timer.
## Feature-Packed Example
```
import "io" for File, Directory, Stdin
import "timer" for Timer
import "scheduler" for Scheduler
import "random" for Random
import "meta" for Meta
import "os" for Process
class Demo is Sequence {
construct new() { _list = [1,2,3] }
iterate(i) { _list.iterate(i) }
iteratorValue(i) { _list.iteratorValue(i) * 2 }
}
var rand = Random.new()
System.print(rand.int(10)) // Random num
var seq = Demo.new()
System.print(seq.map { |x| x + 1 }.toList) // [3,5,7]
Scheduler.add {
Timer.sleep(100)
System.print("Async fiber")
}
var modVars = Meta.getModuleVariables("io")
System.print(modVars) // Module vars
var file = "temp.txt"
File.create(file) { |f| f.writeBytes("data") }
System.print(File.read(file)) // data
File.delete(file)
var sub = Subprocess.run("echo hello")
System.print(sub.stdout.trim()) // hello
System.print(Process.args) // Args
```
+97
View File
@@ -0,0 +1,97 @@
// wren source code generator v1.0
// Generation time: 0.101419
import "io" for File, Directory, Stdin, Stdout
import "sqlite" for Database
class Phonebook {
construct new() {
_db = Database.memory()
init_()
}
init_() {
_db.execute("CREATE TABLE IF NOT EXISTS contacts (id INTEGER PRIMARY KEY, name TEXT, phone TEXT)", [])
}
add(name, phone) {
_db.execute("INSERT INTO contacts (name, phone) VALUES (?, ?)", [name, phone])
System.print("Contact added: %(name)")
}
list() {
var rows = _db.query("SELECT * FROM contacts", [])
if (rows.isEmpty) {
System.print("No contacts found.")
return
}
System.print("\n--- Phonebook ---")
for (row in rows) {
System.print("%(row["name"]) : %(row["phone"])")
}
System.print("")
}
search(name) {
var rows = _db.query("SELECT * FROM contacts WHERE name LIKE ?", ["\%" + name + "\%"])
if (rows.isEmpty) {
System.print("No contacts found for '%(name)'.")
return
}
System.print("\n--- Search Results ---")
for (row in rows) {
System.print("%(row[1]) : %(row[2])")
}
System.print("")
}
delete(name) {
_db.execute("DELETE FROM contacts WHERE name = ?", [name])
System.print("Contact deleted: %(name)")
}
run() {
System.print("\n=== CLI Phonebook ===")
var running = true
while (running) {
System.print("\n1. Add contact")
System.print("2. List contacts")
System.print("3. Search contact")
System.print("4. Delete contact")
System.print("5. Exit")
System.write("Choose option: ")
Stdout.flush()
var choice = Stdin.readLine().trim()
if (choice == "1") {
System.write("Name: ")
Stdout.flush()
var name = Stdin.readLine().trim()
System.write("Phone: ")
Stdout.flush()
var phone = Stdin.readLine().trim()
add(name, phone)
} else if (choice == "2") {
list()
} else if (choice == "3") {
System.write("Search name: ")
Stdout.flush()
var name = Stdin.readLine().trim()
search(name)
} else if (choice == "4") {
System.write("Contact name to delete: ")
Stdout.flush()
var name = Stdin.readLine().trim()
delete(name)
} else if (choice == "5") {
System.print("Goodbye!")
running = false
} else {
System.print("Invalid option.")
}
}
}
}
var pb = Phonebook.new()
pb.run()