This commit is contained in:
retoor 2026-01-25 10:50:20 +01:00
parent e4a94d576b
commit 735596fdf6
48 changed files with 1543 additions and 86 deletions

View File

@ -11,6 +11,9 @@ debug:
tests: build
python3 util/test.py
create_merge:
wren apps/merge_all_wren.wren
create_docs:
wren apps/merge_docs.wren

146
apps/generator.wren vendored Executable file
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
apps/minitut.md Normal file
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
apps/phonebook.wren vendored Normal file
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()

View File

@ -2534,7 +2534,52 @@ static void await_(Compiler* compiler, bool canAssign)
emitShortArg(compiler, CODE_LOAD_MODULE_VAR, schedulerSymbol);
ignoreNewlines(compiler);
expression(compiler);
if (peek(compiler) == TOKEN_NAME)
{
Token nameToken = compiler->parser->current;
Variable variable = resolveNonmodule(compiler, nameToken.start, nameToken.length);
if (variable.index == -1)
{
variable.scope = SCOPE_MODULE;
variable.index = wrenSymbolTableFind(&compiler->parser->module->variableNames,
nameToken.start, nameToken.length);
}
if (variable.index != -1)
{
TokenType following = peekNext(compiler);
if (following == TOKEN_LEFT_PAREN)
{
nextToken(compiler->parser);
loadVariable(compiler, variable);
Signature signature = { "call", 4, SIG_GETTER, 0 };
methodCall(compiler, CODE_CALL_0, &signature);
allowLineBeforeDot(compiler);
while (match(compiler, TOKEN_DOT))
{
namedCall(compiler, false, CODE_CALL_0);
allowLineBeforeDot(compiler);
}
}
else
{
expression(compiler);
}
}
else
{
expression(compiler);
}
}
else
{
expression(compiler);
}
callMethod(compiler, 1, "await_(_)", 9);
}
@ -2544,18 +2589,45 @@ static void async_(Compiler* compiler, bool canAssign)
int schedulerSymbol = resolveScheduler(compiler);
if (schedulerSymbol == -1) return;
emitShortArg(compiler, CODE_LOAD_MODULE_VAR, schedulerSymbol);
consume(compiler, TOKEN_LEFT_BRACE, "Expect '{' after 'async'.");
Compiler fnCompiler;
initCompiler(&fnCompiler, compiler->parser, compiler, false);
fnCompiler.fn->arity = 0;
if (match(compiler, TOKEN_PIPE))
{
Compiler outerCompiler;
initCompiler(&outerCompiler, compiler->parser, compiler, false);
finishBody(&fnCompiler);
endCompiler(&fnCompiler, "[async]", 7);
Signature outerSignature = { "", 0, SIG_METHOD, 0 };
finishParameterList(&outerCompiler, &outerSignature);
consume(compiler, TOKEN_PIPE, "Expect '|' after parameters.");
outerCompiler.fn->arity = outerSignature.arity;
callMethod(compiler, 1, "async_(_)", 9);
emitShortArg(&outerCompiler, CODE_LOAD_MODULE_VAR, schedulerSymbol);
Compiler innerCompiler;
initCompiler(&innerCompiler, outerCompiler.parser, &outerCompiler, false);
innerCompiler.fn->arity = 0;
finishBody(&innerCompiler);
endCompiler(&innerCompiler, "[async]", 7);
callMethod(&outerCompiler, 1, "async_(_)", 9);
emitOp(&outerCompiler, CODE_RETURN);
endCompiler(&outerCompiler, "[async fn]", 10);
}
else
{
emitShortArg(compiler, CODE_LOAD_MODULE_VAR, schedulerSymbol);
Compiler fnCompiler;
initCompiler(&fnCompiler, compiler->parser, compiler, false);
fnCompiler.fn->arity = 0;
finishBody(&fnCompiler);
endCompiler(&fnCompiler, "[async]", 7);
callMethod(compiler, 1, "async_(_)", 9);
}
}
// Subscript or "array indexing" operator like `foo[bar]`.

View File

@ -3,20 +3,239 @@
import "scheduler" for Scheduler, Future
import "timer" for Timer
System.print("=== Await Demo ===\n")
System.print("=== Async/Await Demo ===\n")
System.print("--- Basic Await ---")
await Timer.sleep(1)
System.print("Timer completed")
System.print("Timer completed after 1ms")
System.print("\n--- Concurrent Async ---")
var task1 = async { Timer.sleep(1) }
var task2 = async { Timer.sleep(1) }
await task1
await task2
System.print("Both tasks completed concurrently")
var getValue = async { 42 }
var result = await getValue()
System.print("Async block returned: %(result)")
System.print("\n--- Async With Result ---")
var task = async { "computed value" }
var result = await task
System.print("Result: %(result)")
System.print("\n--- Parameterized Async Functions ---")
var double = async { |x| x * 2 }
var add = async { |a, b| a + b }
var multiply = async { |a, b| a * b }
System.print("double(21) = %(await double(21))")
System.print("add(3, 4) = %(await add(3, 4))")
System.print("multiply(6, 7) = %(await multiply(6, 7))")
System.print("\n--- Nested Awaits ---")
var addTen = async { |x| x + 10 }
System.print("addTen(double(5)) = %(await addTen(await double(5)))")
System.print("double(double(double(2))) = %(await double(await double(await double(2))))")
System.print("\n--- Await in Expressions ---")
System.print("1 + double(5) = %(1 + await double(5))")
System.print("double(3) + double(4) = %(await double(3) + await double(4))")
System.print("\n--- Await in Conditions ---")
var isPositive = async { |x| x > 0 }
if (await isPositive(5)) System.print("5 is positive: true")
var ternaryResult = await isPositive(10) ? "yes" : "no"
System.print("10 is positive? %(ternaryResult)")
System.print("\n--- Await in Loops ---")
var squares = []
for (i in 1..5) {
var sq = async { |x| x * x }
squares.add(await sq(i))
}
System.print("Squares of 1-5: %(squares)")
System.print("\n=== Class-Based Async Patterns ===\n")
System.print("--- Static Async Getters ---")
class Calculator {
static add { async { |a, b| a + b } }
static multiply { async { |a, b| a * b } }
static square { async { |x| x * x } }
static pi { async { 3.14159 } }
static compute(a, b) {
var addFn = Calculator.add
var multiplyFn = Calculator.multiply
var sum = await addFn(a, b)
var product = await multiplyFn(a, b)
return {"sum": sum, "product": product}
}
}
var addFn = Calculator.add
var multiplyFn = Calculator.multiply
var squareFn = Calculator.square
var piFn = Calculator.pi
System.print("add(3, 4) = %(await addFn(3, 4))")
System.print("multiply(5, 6) = %(await multiplyFn(5, 6))")
System.print("square(8) = %(await squareFn(8))")
System.print("pi() = %(await piFn())")
var results = Calculator.compute(5, 3)
System.print("Calculator.compute(5, 3) = %(results)")
System.print("\n--- Instance Methods with Async ---")
class Counter {
construct new(start) {
_value = start
}
value { _value }
add(n) {
var op = async { |x|
_value = _value + x
return _value
}
return await op(n)
}
subtract(n) {
var op = async { |x|
_value = _value - x
return _value
}
return await op(n)
}
reset() {
var op = async {
_value = 0
return _value
}
return await op()
}
}
var counter = Counter.new(100)
System.print("Initial value: %(counter.value)")
System.print("After add(25): %(counter.add(25))")
System.print("After subtract(10): %(counter.subtract(10))")
System.print("After reset(): %(counter.reset())")
System.print("Final value: %(counter.value)")
System.print("\n--- Mock API Service ---")
class UserApi {
static fetchUser { async { |id|
Timer.sleep(1)
return {"id": id, "name": "User%(id)", "email": "user%(id)@example.com"}
}}
static fetchPosts { async { |userId|
Timer.sleep(1)
return [
{"id": 1, "userId": userId, "title": "First Post"},
{"id": 2, "userId": userId, "title": "Second Post"}
]
}}
static getUserWithPosts(userId) {
var fetchUser = UserApi.fetchUser
var fetchPosts = UserApi.fetchPosts
var user = await fetchUser(userId)
var posts = await fetchPosts(userId)
user["posts"] = posts
return user
}
}
var fetchUser = UserApi.fetchUser
var user = await fetchUser(42)
System.print("User: %(user["name"]) <%(user["email"])>")
var fetchPosts = UserApi.fetchPosts
var posts = await fetchPosts(42)
System.print("Posts count: %(posts.count)")
var fullUser = UserApi.getUserWithPosts(123)
System.print("Full user: %(fullUser["name"]) with %(fullUser["posts"].count) posts")
System.print("\n--- Data Pipeline ---")
class DataPipeline {
static parse { async { |csv| csv.split(",") } }
static filter { async { |list, predicate|
var result = []
for (item in list) {
if (predicate.call(item)) result.add(item)
}
return result
}}
static transform { async { |list, mapper|
var result = []
for (item in list) {
result.add(mapper.call(item))
}
return result
}}
static reduce { async { |list, initial, reducer|
var acc = initial
for (item in list) {
acc = reducer.call(acc, item)
}
return acc
}}
static process(csv) {
var parse = DataPipeline.parse
var filter = DataPipeline.filter
var transform = DataPipeline.transform
var reduce = DataPipeline.reduce
var items = await parse(csv)
var filtered = await filter(items) { |s| s.count > 3 }
var lengths = await transform(filtered) { |s| s.count }
var total = await reduce(lengths, 0) { |acc, n| acc + n }
return total
}
}
var data = "one,two,three,four,five,six,seven"
var parse = DataPipeline.parse
var parsed = await parse(data)
System.print("Parsed: %(parsed)")
var filter = DataPipeline.filter
var filtered = await filter(parsed) { |s| s.count > 3 }
System.print("Filtered (length > 3): %(filtered)")
var transform = DataPipeline.transform
var lengths = await transform(filtered) { |s| s.count }
System.print("Lengths: %(lengths)")
var reduce = DataPipeline.reduce
var sum = await reduce(lengths, 0) { |acc, n| acc + n }
System.print("Sum of lengths: %(sum)")
var total = DataPipeline.process("apple,banana,cherry,date,elderberry")
System.print("Pipeline result: %(total)")
System.print("\n--- Parallel Task Execution ---")
var fetchA = async { |x|
Timer.sleep(10)
return "A:%(x)"
}
var fetchB = async { |x|
Timer.sleep(10)
return "B:%(x)"
}
var fetchC = async { |x|
Timer.sleep(10)
return "C:%(x)"
}
var futureA = fetchA.call(1)
var futureB = fetchB.call(2)
var futureC = fetchC.call(3)
System.print("Started 3 parallel tasks")
System.print("Results: %(await futureA), %(await futureB), %(await futureC)")
System.print("\n=== Demo Complete ===")

View File

@ -39,7 +39,15 @@ System.print("Status: %(response.statusCode)")
System.print("")
if (response.ok) {
var data = Json.parse(response.body)
let text = response.body
if(text.startsWith("```")){
text = text.trim("```")
text = text.trim("json")
text = text.trim("\n")
}
System.print(text)
var data = Json.parse(text)
if (data != null && data["choices"] != null && data["choices"].count > 0) {
var message = data["choices"][0]["message"]
if (message != null && message["content"] != null) {

View File

@ -126,6 +126,99 @@
</div>
<p>Resumes a fiber with a result value.</p>
<h2 id="await-syntax">Await Syntax</h2>
<p>Wren-CLI provides a convenient syntax for awaiting async functions. Instead of using <code>.call()</code>, you can call async functions directly after <code>await</code>:</p>
<h3>Before and After</h3>
<pre><code>// Old syntax (still works)
var result = await asyncFn.call(arg1, arg2)
// New syntax (recommended)
var result = await asyncFn(arg1, arg2)</code></pre>
<p>The compiler automatically translates <code>await fn(args)</code> to <code>await fn.call(args)</code>.</p>
<h3>Supported Patterns</h3>
<table class="params-table">
<thead>
<tr><th>Pattern</th><th>Example</th></tr>
</thead>
<tbody>
<tr><td>No arguments</td><td><code>await getValue()</code></td></tr>
<tr><td>Single argument</td><td><code>await double(21)</code></td></tr>
<tr><td>Multiple arguments</td><td><code>await add(3, 4, 5)</code></td></tr>
<tr><td>Nested awaits</td><td><code>await outer(await inner(x))</code></td></tr>
<tr><td>In expressions</td><td><code>var x = 1 + await fn(2)</code></td></tr>
<tr><td>In conditions</td><td><code>if (await check(x)) { }</code></td></tr>
<tr><td>Block argument</td><td><code>await map(list) { |x| x * 2 }</code></td></tr>
</tbody>
</table>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>Method chaining after <code>await fn(args)</code> requires assigning the result first:</p>
<pre><code>var list = await getList(5)
System.print(list.count)</code></pre>
</div>
<h3>Examples</h3>
<pre><code>import "scheduler" for Scheduler, Future
var double = async { |x| x * 2 }
var add = async { |a, b| a + b }
// Basic usage
System.print(await double(21)) // 42
System.print(await add(3, 4)) // 7
// Nested awaits
var result = await double(await add(5, 5)) // 20
// Method chaining
var getList = async { |n| [1, 2, 3, n] }
System.print(await getList(4).count) // 4
// In expressions
var total = await add(10, 20) + await double(5) // 40</code></pre>
<h3>Class-Based Async Patterns</h3>
<p>Static getters that return async functions must first be assigned to a variable:</p>
<pre><code>import "scheduler" for Scheduler, Future
class Calculator {
static add { async { |a, b| a + b } }
static multiply { async { |a, b| a * b } }
static square { async { |x| x * x } }
static compute(a, b) {
var addFn = Calculator.add
var multiplyFn = Calculator.multiply
var sum = await addFn(a, b)
var product = await multiplyFn(a, b)
return [sum, product]
}
}
// Assign getter to variable first, then await
var add = Calculator.add
System.print(await add(3, 4)) // 7
var multiply = Calculator.multiply
System.print(await multiply(5, 6)) // 30
var square = Calculator.square
System.print(await square(8)) // 64</code></pre>
<div class="admonition note">
<div class="admonition-title">Note</div>
<p>The <code>fn(args)</code> syntax only works directly after <code>await</code>. Outside of <code>await</code>, use <code>fn.call(args)</code> to invoke async functions.</p>
</div>
<h2>How Async Works</h2>
<p>Wren-CLI uses an event loop (libuv) for non-blocking I/O. Here is how async operations work:</p>

24
src/module/http.wren vendored
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) {

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"

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
src/module/io.wren vendored
View File

@ -280,6 +280,7 @@ class Stdin {
} else {
__waitingFiber.transferError("Stdin was closed.")
}
return
}
// Append to the buffer.

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"

12
src/module/repl.wren vendored
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
}

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"

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_()

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";

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;
}
}
}

4
src/module/web.wren vendored
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()

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"

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
}

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"

8
test/await/async_parameterized.wren vendored Normal file
View File

@ -0,0 +1,8 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var delayed = async { |ms| ms }
System.print(await delayed(1)) // expect: 1
System.print(await delayed(2)) // expect: 2

View File

@ -0,0 +1,7 @@
import "scheduler" for Scheduler, Future
var base = 100
var addBase = async { |x| base + x }
var f = addBase.call(42)
System.print(await f) // expect: 142

View File

@ -0,0 +1,17 @@
import "scheduler" for Scheduler, Future
import "timer" for Timer
var order = []
var task = async { |name, ms|
await Timer.sleep(ms)
order.add(name)
name
}
var f1 = task.call("slow", 2)
var f2 = task.call("fast", 1)
await f1
await f2
System.print(order) // expect: [fast, slow]

View File

@ -0,0 +1,7 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var add = async { |a, b| a + b }
System.print(await add(3, 4)) // expect: 7

17
test/await/await_fn_block_arg.wren vendored Normal file
View File

@ -0,0 +1,17 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var transform = async { |list, fn|
var result = []
for (item in list) {
result.add(fn.call(item))
}
return result
}
var doubled = await transform([1, 2, 3]) { |x| x * 2 }
System.print(doubled) // expect: [2, 4, 6]
var squared = await transform([2, 3, 4]) { |x| x * x }
System.print(squared) // expect: [4, 9, 16]

7
test/await/await_fn_call.wren vendored Normal file
View File

@ -0,0 +1,7 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var double = async { |x| x * 2 }
System.print(await double(21)) // expect: 42

18
test/await/await_fn_chained.wren vendored Normal file
View File

@ -0,0 +1,18 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var getList = async { |x| [x, x * 2, x * 3] }
var getString = async { |s| s }
var list = await getList(5)
System.print(list.count) // expect: 3
var list2 = await getList(10)
System.print(list2[1]) // expect: 20
var str = await getString("hello")
System.print(str.count) // expect: 5
var str2 = await getString("world")
System.print(str2.toList[0]) // expect: w

50
test/await/await_fn_class_api.wren vendored Normal file
View File

@ -0,0 +1,50 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
import "timer" for Timer
class MockApi {
static fetchUser { async { |id|
Timer.sleep(1)
return {"id": id, "name": "User%(id)", "active": true}
}}
static fetchPosts { async { |userId|
Timer.sleep(1)
return [
{"id": 1, "userId": userId, "title": "Post 1"},
{"id": 2, "userId": userId, "title": "Post 2"}
]
}}
static createPost { async { |userId, title|
Timer.sleep(1)
return {"id": 3, "userId": userId, "title": title, "created": true}
}}
static getUserWithPosts(userId) {
var fetchUser = MockApi.fetchUser
var fetchPosts = MockApi.fetchPosts
var user = await fetchUser(userId)
var posts = await fetchPosts(userId)
user["posts"] = posts
return user
}
}
var fetchUser = MockApi.fetchUser
var user = await fetchUser(42)
System.print(user["name"]) // expect: User42
var fetchPosts = MockApi.fetchPosts
var posts = await fetchPosts(42)
System.print(posts.count) // expect: 2
var createPost = MockApi.createPost
var newPost = await createPost(42, "New Post")
System.print(newPost["created"]) // expect: true
var fullUser = MockApi.getUserWithPosts(99)
System.print(fullUser["name"]) // expect: User99
System.print(fullUser["posts"].count) // expect: 2

54
test/await/await_fn_class_chain.wren vendored Normal file
View File

@ -0,0 +1,54 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
class DataProcessor {
static parse { async { |text| text.split(",") } }
static filter { async { |list, fn|
var result = []
for (item in list) {
if (fn.call(item)) result.add(item)
}
return result
}}
static transform { async { |list, fn|
var result = []
for (item in list) {
result.add(fn.call(item))
}
return result
}}
static join { async { |list, sep| list.join(sep) } }
static pipeline(data) {
var parse = DataProcessor.parse
var filter = DataProcessor.filter
var transform = DataProcessor.transform
var join = DataProcessor.join
var parsed = await parse(data)
var filtered = await filter(parsed) { |s| s.count > 4 }
var transformed = await transform(filtered) { |s| s.toList[0] }
return await join(transformed, "-")
}
}
var data = "apple,banana,cherry,date"
var parse = DataProcessor.parse
var parsed = await parse(data)
System.print(parsed) // expect: [apple, banana, cherry, date]
var filter = DataProcessor.filter
var filtered = await filter(parsed) { |s| s.count > 5 }
System.print(filtered) // expect: [banana, cherry]
var transform = DataProcessor.transform
var transformed = await transform(filtered) { |s| s.toList[0] }
System.print(transformed) // expect: [b, c]
var join = DataProcessor.join
var joined = await join(transformed, "-")
System.print(joined) // expect: b-c
var result = DataProcessor.pipeline("one,two,three,four,five")
System.print(result) // expect: t

50
test/await/await_fn_class_instance.wren vendored Normal file
View File

@ -0,0 +1,50 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
class Counter {
construct new(start) {
_value = start
}
value { _value }
add(n) {
var op = async { |x|
_value = _value + x
return _value
}
return await op(n)
}
subtract(n) {
var op = async { |x|
_value = _value - x
return _value
}
return await op(n)
}
reset() {
var op = async {
_value = 0
return _value
}
return await op()
}
}
var counter = Counter.new(10)
System.print(counter.value) // expect: 10
System.print(counter.add(5)) // expect: 15
System.print(counter.value) // expect: 15
System.print(counter.subtract(3)) // expect: 12
System.print(counter.value) // expect: 12
System.print(counter.reset()) // expect: 0
System.print(counter.value) // expect: 0
System.print(counter.add(10)) // expect: 10
System.print(counter.subtract(3)) // expect: 7

65
test/await/await_fn_class_service.wren vendored Normal file
View File

@ -0,0 +1,65 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
import "timer" for Timer
class CacheService {
construct new() {
_cache = {}
}
get(key) {
var op = async { |k|
Timer.sleep(1)
return _cache.containsKey(k) ? _cache[k] : null
}
return await op(key)
}
set(key, value) {
var op = async { |k, v|
Timer.sleep(1)
_cache[k] = v
return true
}
return await op(key, value)
}
has(key) {
var op = async { |k|
return _cache.containsKey(k)
}
return await op(key)
}
clear() {
var op = async {
_cache = {}
return true
}
return await op()
}
getOrSet(key, defaultValue) {
if (this.has(key)) {
return this.get(key)
}
this.set(key, defaultValue)
return defaultValue
}
}
var cache = CacheService.new()
System.print(cache.has("foo")) // expect: false
System.print(cache.get("foo")) // expect: null
System.print(cache.set("foo", "bar")) // expect: true
System.print(cache.has("foo")) // expect: true
System.print(cache.get("foo")) // expect: bar
System.print(cache.clear()) // expect: true
System.print(cache.has("foo")) // expect: false
System.print(cache.getOrSet("baz", "default")) // expect: default
System.print(cache.get("baz")) // expect: default

33
test/await/await_fn_class_static.wren vendored Normal file
View File

@ -0,0 +1,33 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
class Calculator {
static add { async { |a, b| a + b } }
static multiply { async { |a, b| a * b } }
static square { async { |x| x * x } }
static constant { async { 42 } }
static compute(a, b) {
var addFn = Calculator.add
var multiplyFn = Calculator.multiply
var sum = await addFn(a, b)
var product = await multiplyFn(a, b)
return [sum, product]
}
}
var add = Calculator.add
System.print(await add(3, 4)) // expect: 7
var multiply = Calculator.multiply
System.print(await multiply(5, 6)) // expect: 30
var square = Calculator.square
System.print(await square(8)) // expect: 64
var constant = Calculator.constant
System.print(await constant()) // expect: 42
var results = Calculator.compute(2, 3)
System.print(results) // expect: [5, 6]

View File

@ -0,0 +1,23 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var multiplier = 10
var scale = async { |x| x * multiplier }
System.print(await scale(5)) // expect: 50
multiplier = 100
System.print(await scale(5)) // expect: 500
var prefix = "Result: "
var format = async { |x| prefix + x.toString }
System.print(await format(42)) // expect: Result: 42
var outer = "outer"
var fn = Fn.new {
var inner = "inner"
var capture = async { |x| outer + "-" + inner + "-" + x }
return await capture("arg")
}
System.print(fn.call()) // expect: outer-inner-arg

24
test/await/await_fn_in_condition.wren vendored Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var isPositive = async { |x| x > 0 }
var getValue = async { |x| x }
if (await isPositive(5)) {
System.print("positive") // expect: positive
}
if (await isPositive(-5)) {
System.print("unreachable")
} else {
System.print("negative") // expect: negative
}
var result = await isPositive(10) ? "yes" : "no"
System.print(result) // expect: yes
while (await getValue(false)) {
System.print("unreachable")
}
System.print("after while") // expect: after while

12
test/await/await_fn_in_expression.wren vendored Normal file
View File

@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var getValue = async { |x| x }
var double = async { |x| x * 2 }
System.print(1 + await getValue(2)) // expect: 3
System.print(await double(3) + await double(4)) // expect: 14
System.print(await getValue(10) * 5) // expect: 50
System.print(100 - await getValue(30)) // expect: 70
System.print((await getValue(4)) * (await getValue(5))) // expect: 20

17
test/await/await_fn_in_loop.wren vendored Normal file
View File

@ -0,0 +1,17 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var double = async { |x| x * 2 }
var sum = 0
for (i in [1, 2, 3, 4, 5]) {
sum = sum + await double(i)
}
System.print(sum) // expect: 30
var results = []
for (i in 1..3) {
results.add(await double(i))
}
System.print(results) // expect: [2, 4, 6]

21
test/await/await_fn_local_var.wren vendored Normal file
View File

@ -0,0 +1,21 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
class Test {
static run() {
var localDouble = async { |x| x * 2 }
var localAdd = async { |a, b| a + b }
System.print(await localDouble(5)) // expect: 10
System.print(await localAdd(3, 4)) // expect: 7
var nested = Fn.new {
var innerFn = async { |x| x * 100 }
return await innerFn(3)
}
System.print(nested.call()) // expect: 300
}
}
Test.run()

9
test/await/await_fn_many_args.wren vendored Normal file
View File

@ -0,0 +1,9 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var sum4 = async { |a, b, c, d| a + b + c + d }
var sum5 = async { |a, b, c, d, e| a + b + c + d + e }
System.print(await sum4(1, 2, 3, 4)) // expect: 10
System.print(await sum5(1, 2, 3, 4, 5)) // expect: 15

17
test/await/await_fn_mixed_syntax.wren vendored Normal file
View File

@ -0,0 +1,17 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var double = async { |x| x * 2 }
var triple = async { |x| x * 3 }
System.print(await double(5)) // expect: 10
System.print(await double.call(5)) // expect: 10
var f1 = double.call(10)
var f2 = triple.call(10)
System.print(await f1) // expect: 20
System.print(await f2) // expect: 30
System.print(await double(await triple.call(2))) // expect: 12
System.print(await double.call(await triple(2))) // expect: 12

20
test/await/await_fn_module_var.wren vendored Normal file
View File

@ -0,0 +1,20 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var moduleDouble = async { |x| x * 2 }
var moduleAdd = async { |a, b| a + b }
class Helper {
static useModuleVar(x) {
return await moduleDouble(x)
}
static combineModuleVars(a, b) {
return await moduleAdd(await moduleDouble(a), await moduleDouble(b))
}
}
System.print(await moduleDouble(10)) // expect: 20
System.print(Helper.useModuleVar(15)) // expect: 30
System.print(Helper.combineModuleVars(5, 10)) // expect: 30

10
test/await/await_fn_nested.wren vendored Normal file
View File

@ -0,0 +1,10 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var double = async { |x| x * 2 }
var addTen = async { |x| x + 10 }
System.print(await addTen(await double(5))) // expect: 20
System.print(await double(await addTen(3))) // expect: 26
System.print(await double(await double(await double(2)))) // expect: 16

7
test/await/await_fn_no_args.wren vendored Normal file
View File

@ -0,0 +1,7 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var getValue = async { 42 }
System.print(await getValue()) // expect: 42

30
test/await/await_fn_parallel.wren vendored Normal file
View File

@ -0,0 +1,30 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
import "timer" for Timer
var delayedValue = async { |ms, val|
Timer.sleep(ms)
return val
}
var f1 = delayedValue.call(10, "first")
var f2 = delayedValue.call(10, "second")
var f3 = delayedValue.call(10, "third")
System.print(await f1) // expect: first
System.print(await f2) // expect: second
System.print(await f3) // expect: third
var compute = async { |x| x * x }
var futures = []
for (i in 1..5) {
futures.add(compute.call(i))
}
var results = []
for (f in futures) {
results.add(await f)
}
System.print(results) // expect: [1, 4, 9, 16, 25]

18
test/await/await_fn_return_types.wren vendored Normal file
View File

@ -0,0 +1,18 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var getString = async { |x| x.toString }
var getNum = async { |x| x * 2 }
var getList = async { |x| [x, x, x] }
var getMap = async { |k, v| {k: v} }
var getNull = async { null }
var getBool = async { |x| x > 0 }
System.print(await getString("hello")) // expect: hello
System.print(await getNum(21)) // expect: 42
System.print(await getList(7)) // expect: [7, 7, 7]
System.print(await getMap("key", 123)) // expect: {key: 123}
System.print(await getNull()) // expect: null
System.print(await getBool(5)) // expect: true
System.print(await getBool(-5)) // expect: false

25
test/await/await_fn_sequential.wren vendored Normal file
View File

@ -0,0 +1,25 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var step1 = async { |x| x + 10 }
var step2 = async { |x| x * 2 }
var step3 = async { |x| x - 5 }
var result = 5
result = await step1(result)
System.print(result) // expect: 15
result = await step2(result)
System.print(result) // expect: 30
result = await step3(result)
System.print(result) // expect: 25
var pipeline = async { |x|
var r1 = await step1(x)
var r2 = await step2(r1)
var r3 = await step3(r2)
return r3
}
System.print(await pipeline(0)) // expect: 15
System.print(await pipeline(10)) // expect: 35

28
test/await/await_fn_upvalue.wren vendored Normal file
View File

@ -0,0 +1,28 @@
// retoor <retoor@molodetz.nl>
import "scheduler" for Scheduler, Future
var makeMultiplier = Fn.new { |factor|
var multiply = async { |x| x * factor }
return multiply
}
var times2 = makeMultiplier.call(2)
var times10 = makeMultiplier.call(10)
System.print(await times2(5)) // expect: 10
System.print(await times10(5)) // expect: 50
var counter = 0
var makeCounter = Fn.new {
var increment = async { |step|
counter = counter + step
return counter
}
return increment
}
var inc = makeCounter.call()
System.print(await inc(1)) // expect: 1
System.print(await inc(5)) // expect: 6
System.print(await inc(10)) // expect: 16