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:
+146
@@ -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
@@ -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
|
||||
```
|
||||
Vendored
+97
@@ -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()
|
||||
Reference in New Issue
Block a user