feat: add async/await keywords, Num/String extensions, and Jinja markdown example

Add `async` and `await` token support to the Wren compiler with scheduler resolution logic, extend `Num` with `e` constant, hyperbolic trig, base conversion, and new methods (`isZero`, `gcd`, `lcm`, `digits`, etc.), extend `String` with `lower`, `upper`, `capitalize`, `title`, and regenerate `wren_core.wren.inc`. Include `Makefile` for multi-platform builds, rewrite `README.md` with updated build instructions, and add `example/await_demo.wren` and `example/jinja_markdown.wren` demonstrating new features.
This commit is contained in:
2026-01-25 03:58:39 +00:00
parent 74ef5cbc11
commit 46fc2e2470
139 changed files with 5078 additions and 73 deletions
+3
View File
@@ -28,6 +28,7 @@
#include "signal.wren.inc"
#include "sqlite.wren.inc"
#include "subprocess.wren.inc"
#include "tempfile.wren.inc"
#include "timer.wren.inc"
#include "tls.wren.inc"
#include "uuid.wren.inc"
@@ -504,6 +505,8 @@ static ModuleRegistry modules[] =
STATIC_METHOD("run_(_,_)", subprocessRun)
END_CLASS
END_MODULE
MODULE(tempfile)
END_MODULE
MODULE(timer)
CLASS(Timer)
STATIC_METHOD("startTimer_(_,_)", timerStartTimer)
+1 -1
View File
@@ -156,7 +156,7 @@ void directoryCreate(WrenVM* vm)
{
const char* path = wrenGetSlotString(vm, 1);
uv_fs_t* request = createRequest(wrenGetSlotHandle(vm, 2));
uv_fs_mkdir(getLoop(), request, path, 0, fileDirectoryCallback);
uv_fs_mkdir(getLoop(), request, path, 0777, fileDirectoryCallback);
}
void directoryDelete(WrenVM* vm)
+81
View File
@@ -1,6 +1,7 @@
// retoor <retoor@molodetz.nl>
import "io" for File
import "markdown" for Markdown
class Token {
static TEXT { "TEXT" }
@@ -745,6 +746,34 @@ class FilterBlockNode is Node {
}
}
class MarkdownToHtmlNode is Node {
construct new(body) {
_body = body
}
render(ctx) {
var content = ""
for (node in _body) {
content = content + node.render(ctx)
}
return Markdown.toHtml(content)
}
}
class MarkdownFromHtmlNode is Node {
construct new(body) {
_body = body
}
render(ctx) {
var content = ""
for (node in _body) {
content = content + node.render(ctx)
}
return Markdown.fromHtml(content)
}
}
class Expression {
evaluate(ctx) { null }
}
@@ -1172,6 +1201,8 @@ class Filters {
if (name == "indent") return indent(value, args.count > 0 ? args[0] : 4, args.count > 1 ? args[1] : false)
if (name == "tojson") return tojson(value)
if (name == "debug") return debug(value)
if (name == "markdown") return SafeString.new(Markdown.toHtml(value.toString))
if (name == "markdownfromhtml") return Markdown.fromHtml(value.toString)
Fiber.abort(TemplateError.new("Unknown filter: %(name)", 0, 0).toString)
}
@@ -1801,6 +1832,8 @@ class Parser {
if (tagName == "macro") return parseMacro_()
if (tagName == "raw") return parseRaw_()
if (tagName == "filter") return parseFilterBlock_()
if (tagName == "markdowntohtml") return parseMarkdownToHtml_()
if (tagName == "markdownfromhtml") return parseMarkdownFromHtml_()
Fiber.abort(TemplateError.new("Unknown block tag: %(tagName)", nameToken.line, nameToken.col).toString)
}
@@ -2180,6 +2213,54 @@ class Parser {
Fiber.abort(TemplateError.new("Unclosed filter tag", 0, 0).toString)
}
parseMarkdownToHtml_() {
consumeBlockEnd_()
var body = []
while (!isAtEnd_()) {
var token = peek_()
if (token.type == Token.BLOCK_START || token.type == Token.BLOCK_START_TRIM) {
advance_()
var nextName = peek_()
if (nextName.type == Token.NAME && nextName.value == "endmarkdowntohtml") {
advance_()
consumeBlockEnd_()
return MarkdownToHtmlNode.new(body)
}
_pos = _pos - 1
var innerNode = parseInnerNode_()
if (innerNode != null) body.add(innerNode)
} else {
var innerNode = parseInnerNode_()
if (innerNode != null) body.add(innerNode)
}
}
Fiber.abort(TemplateError.new("Unclosed markdowntohtml tag", 0, 0).toString)
}
parseMarkdownFromHtml_() {
consumeBlockEnd_()
var body = []
while (!isAtEnd_()) {
var token = peek_()
if (token.type == Token.BLOCK_START || token.type == Token.BLOCK_START_TRIM) {
advance_()
var nextName = peek_()
if (nextName.type == Token.NAME && nextName.value == "endmarkdownfromhtml") {
advance_()
consumeBlockEnd_()
return MarkdownFromHtmlNode.new(body)
}
_pos = _pos - 1
var innerNode = parseInnerNode_()
if (innerNode != null) body.add(innerNode)
} else {
var innerNode = parseInnerNode_()
if (innerNode != null) body.add(innerNode)
}
}
Fiber.abort(TemplateError.new("Unclosed markdownfromhtml tag", 0, 0).toString)
}
parseInnerNode_() {
var token = peek_()
+81
View File
@@ -5,6 +5,7 @@ static const char* jinjaModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"io\" for File\n"
"import \"markdown\" for Markdown\n"
"\n"
"class Token {\n"
" static TEXT { \"TEXT\" }\n"
@@ -749,6 +750,34 @@ static const char* jinjaModuleSource =
" }\n"
"}\n"
"\n"
"class MarkdownToHtmlNode is Node {\n"
" construct new(body) {\n"
" _body = body\n"
" }\n"
"\n"
" render(ctx) {\n"
" var content = \"\"\n"
" for (node in _body) {\n"
" content = content + node.render(ctx)\n"
" }\n"
" return Markdown.toHtml(content)\n"
" }\n"
"}\n"
"\n"
"class MarkdownFromHtmlNode is Node {\n"
" construct new(body) {\n"
" _body = body\n"
" }\n"
"\n"
" render(ctx) {\n"
" var content = \"\"\n"
" for (node in _body) {\n"
" content = content + node.render(ctx)\n"
" }\n"
" return Markdown.fromHtml(content)\n"
" }\n"
"}\n"
"\n"
"class Expression {\n"
" evaluate(ctx) { null }\n"
"}\n"
@@ -1176,6 +1205,8 @@ static const char* jinjaModuleSource =
" if (name == \"indent\") return indent(value, args.count > 0 ? args[0] : 4, args.count > 1 ? args[1] : false)\n"
" if (name == \"tojson\") return tojson(value)\n"
" if (name == \"debug\") return debug(value)\n"
" if (name == \"markdown\") return SafeString.new(Markdown.toHtml(value.toString))\n"
" if (name == \"markdownfromhtml\") return Markdown.fromHtml(value.toString)\n"
" Fiber.abort(TemplateError.new(\"Unknown filter: %(name)\", 0, 0).toString)\n"
" }\n"
"\n"
@@ -1805,6 +1836,8 @@ static const char* jinjaModuleSource =
" if (tagName == \"macro\") return parseMacro_()\n"
" if (tagName == \"raw\") return parseRaw_()\n"
" if (tagName == \"filter\") return parseFilterBlock_()\n"
" if (tagName == \"markdowntohtml\") return parseMarkdownToHtml_()\n"
" if (tagName == \"markdownfromhtml\") return parseMarkdownFromHtml_()\n"
"\n"
" Fiber.abort(TemplateError.new(\"Unknown block tag: %(tagName)\", nameToken.line, nameToken.col).toString)\n"
" }\n"
@@ -2184,6 +2217,54 @@ static const char* jinjaModuleSource =
" Fiber.abort(TemplateError.new(\"Unclosed filter tag\", 0, 0).toString)\n"
" }\n"
"\n"
" parseMarkdownToHtml_() {\n"
" consumeBlockEnd_()\n"
" var body = []\n"
" while (!isAtEnd_()) {\n"
" var token = peek_()\n"
" if (token.type == Token.BLOCK_START || token.type == Token.BLOCK_START_TRIM) {\n"
" advance_()\n"
" var nextName = peek_()\n"
" if (nextName.type == Token.NAME && nextName.value == \"endmarkdowntohtml\") {\n"
" advance_()\n"
" consumeBlockEnd_()\n"
" return MarkdownToHtmlNode.new(body)\n"
" }\n"
" _pos = _pos - 1\n"
" var innerNode = parseInnerNode_()\n"
" if (innerNode != null) body.add(innerNode)\n"
" } else {\n"
" var innerNode = parseInnerNode_()\n"
" if (innerNode != null) body.add(innerNode)\n"
" }\n"
" }\n"
" Fiber.abort(TemplateError.new(\"Unclosed markdowntohtml tag\", 0, 0).toString)\n"
" }\n"
"\n"
" parseMarkdownFromHtml_() {\n"
" consumeBlockEnd_()\n"
" var body = []\n"
" while (!isAtEnd_()) {\n"
" var token = peek_()\n"
" if (token.type == Token.BLOCK_START || token.type == Token.BLOCK_START_TRIM) {\n"
" advance_()\n"
" var nextName = peek_()\n"
" if (nextName.type == Token.NAME && nextName.value == \"endmarkdownfromhtml\") {\n"
" advance_()\n"
" consumeBlockEnd_()\n"
" return MarkdownFromHtmlNode.new(body)\n"
" }\n"
" _pos = _pos - 1\n"
" var innerNode = parseInnerNode_()\n"
" if (innerNode != null) body.add(innerNode)\n"
" } else {\n"
" var innerNode = parseInnerNode_()\n"
" if (innerNode != null) body.add(innerNode)\n"
" }\n"
" }\n"
" Fiber.abort(TemplateError.new(\"Unclosed markdownfromhtml tag\", 0, 0).toString)\n"
" }\n"
"\n"
" parseInnerNode_() {\n"
" var token = peek_()\n"
"\n"
+5 -1
View File
@@ -205,10 +205,14 @@ static void allocCallback(uv_handle_t* handle, size_t suggestedSize, uv_buf_t* b
static void readCallback(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
{
if (nread == 0) {
free(buf->base);
return;
}
SocketData* data = (SocketData*)stream->data;
WrenHandle* fiber = data->fiber;
// Stop reading immediately. We only want one chunk.
uv_read_stop(stream);
data->fiber = NULL;
+48 -5
View File
@@ -8,15 +8,28 @@ class Scheduler {
})
}
// Called by native code.
static resume_(fiber) { fiber.transfer() }
static resume_(fiber, arg) { fiber.transfer(arg) }
static resumeError_(fiber, error) { fiber.transferError(error) }
// wait for a method to finish that has a callback on the C side
static await_(fn) {
fn.call()
return Scheduler.runNextScheduled_()
static await_(arg) {
if (arg is Fn) {
arg.call()
return Scheduler.runNextScheduled_()
}
if (arg is Future) return arg.await_()
return arg
}
static async_(fn) {
return Future.new(fn)
}
static scheduleTransfer_(fiber, value) {
if (__scheduled == null) __scheduled = []
__scheduled.add(Fiber.new {
fiber.transfer(value)
})
}
static runNextScheduled_() {
@@ -30,4 +43,34 @@ class Scheduler {
foreign static captureMethods_()
}
class Future {
construct new(fn) {
_resolved = false
_result = null
_waiters = []
var self = this
Scheduler.add {
self.resolve_(fn.call())
}
}
resolve_(value) {
_resolved = true
_result = value
for (w in _waiters) {
Scheduler.scheduleTransfer_(w, value)
}
_waiters = []
}
await_() {
if (_resolved) return _result
_waiters.add(Fiber.current)
return Scheduler.runNextScheduled_()
}
isDone { _resolved }
result { _result }
}
Scheduler.captureMethods_()
+48 -5
View File
@@ -12,15 +12,28 @@ static const char* schedulerModuleSource =
" })\n"
" }\n"
"\n"
" // Called by native code.\n"
" static resume_(fiber) { fiber.transfer() }\n"
" static resume_(fiber, arg) { fiber.transfer(arg) }\n"
" static resumeError_(fiber, error) { fiber.transferError(error) }\n"
"\n"
" // wait for a method to finish that has a callback on the C side\n"
" static await_(fn) {\n"
" fn.call()\n"
" return Scheduler.runNextScheduled_()\n"
" static await_(arg) {\n"
" if (arg is Fn) {\n"
" arg.call()\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
" if (arg is Future) return arg.await_()\n"
" return arg\n"
" }\n"
"\n"
" static async_(fn) {\n"
" return Future.new(fn)\n"
" }\n"
"\n"
" static scheduleTransfer_(fiber, value) {\n"
" if (__scheduled == null) __scheduled = []\n"
" __scheduled.add(Fiber.new {\n"
" fiber.transfer(value)\n"
" })\n"
" }\n"
"\n"
" static runNextScheduled_() {\n"
@@ -34,4 +47,34 @@ static const char* schedulerModuleSource =
" foreign static captureMethods_()\n"
"}\n"
"\n"
"class Future {\n"
" construct new(fn) {\n"
" _resolved = false\n"
" _result = null\n"
" _waiters = []\n"
" var self = this\n"
" Scheduler.add {\n"
" self.resolve_(fn.call())\n"
" }\n"
" }\n"
"\n"
" resolve_(value) {\n"
" _resolved = true\n"
" _result = value\n"
" for (w in _waiters) {\n"
" Scheduler.scheduleTransfer_(w, value)\n"
" }\n"
" _waiters = []\n"
" }\n"
"\n"
" await_() {\n"
" if (_resolved) return _result\n"
" _waiters.add(Fiber.current)\n"
" return Scheduler.runNextScheduled_()\n"
" }\n"
"\n"
" isDone { _resolved }\n"
" result { _result }\n"
"}\n"
"\n"
"Scheduler.captureMethods_()\n";
+173
View File
@@ -0,0 +1,173 @@
// retoor <retoor@molodetz.nl>
import "io" for Directory, File, FileFlags
import "os" for Platform
import "env" for Environment
import "crypto" for Crypto
import "strutil" for Str
import "bytes" for Bytes
import "pathlib" for Path
class TempFile {
static tempdir { __tempdir }
static tempdir=(value) { __tempdir = value }
static gettempdir() {
if (__tempdir != null) return __tempdir
var candidates = ["TMPDIR", "TEMP", "TMP"]
for (name in candidates) {
var value = Environment.get(name)
if (value != null && value.count > 0) return value
}
if (Platform.isPosix) return "/tmp"
return "C:\\Temp"
}
static gettempprefix() { "tmp" }
static randomSuffix_() {
var bytes = Crypto.randomBytes(4)
return Str.hexEncode(Bytes.fromList(bytes))
}
static mktemp() { mktemp("", gettempprefix(), gettempdir()) }
static mktemp(suffix) { mktemp(suffix, gettempprefix(), gettempdir()) }
static mktemp(suffix, prefix) { mktemp(suffix, prefix, gettempdir()) }
static mktemp(suffix, prefix, dir) {
var name = prefix + randomSuffix_() + suffix
return dir + "/" + name
}
static mkstemp() { mkstemp("", gettempprefix(), gettempdir()) }
static mkstemp(suffix) { mkstemp(suffix, gettempprefix(), gettempdir()) }
static mkstemp(suffix, prefix) { mkstemp(suffix, prefix, gettempdir()) }
static mkstemp(suffix, prefix, dir) {
var flags = FileFlags.writeOnly | FileFlags.create | FileFlags.exclusive
for (i in 0...100) {
var name = prefix + randomSuffix_() + suffix
var path = dir + "/" + name
var file
var error
var fiber = Fiber.new {
file = File.openWithFlags(path, flags)
}
fiber.try()
if (fiber.error == null && file != null) {
file.close()
return path
}
}
Fiber.abort("Failed to create temporary file after 100 attempts.")
}
static mkdtemp() { mkdtemp("", gettempprefix(), gettempdir()) }
static mkdtemp(suffix) { mkdtemp(suffix, gettempprefix(), gettempdir()) }
static mkdtemp(suffix, prefix) { mkdtemp(suffix, prefix, gettempdir()) }
static mkdtemp(suffix, prefix, dir) {
for (i in 0...100) {
var name = prefix + randomSuffix_() + suffix
var path = dir + "/" + name
var p = Path.new(path)
if (!p.exists()) {
Directory.create(path)
return path
}
}
Fiber.abort("Failed to create temporary directory after 100 attempts.")
}
}
class NamedTemporaryFile {
construct new() { init_("", TempFile.gettempprefix(), TempFile.gettempdir(), true) }
construct new(suffix) { init_(suffix, TempFile.gettempprefix(), TempFile.gettempdir(), true) }
construct new(suffix, prefix) { init_(suffix, prefix, TempFile.gettempdir(), true) }
construct new(suffix, prefix, dir) { init_(suffix, prefix, dir, true) }
construct new(suffix, prefix, dir, delete) { init_(suffix, prefix, dir, delete) }
init_(suffix, prefix, dir, delete) {
var d = dir != null ? dir : TempFile.gettempdir()
_name = TempFile.mkstemp(suffix, prefix, d)
_delete = delete
_closed = false
}
name { _name }
path { Path.new(_name) }
delete { _delete }
delete=(value) { _delete = value }
closed { _closed }
write(content) {
if (_closed) Fiber.abort("File is closed.")
File.openWithFlags(_name, FileFlags.writeOnly | FileFlags.create) {|f|
f.writeBytes(content, 0)
}
}
read() {
if (_closed) Fiber.abort("File is closed.")
return File.read(_name)
}
close() {
if (_closed) return
_closed = true
if (_delete) {
var fiber = Fiber.new { File.delete(_name) }
fiber.try()
}
}
use(fn) {
var fiber = Fiber.new { fn.call(this) }
var result = fiber.try()
close()
if (fiber.error != null) Fiber.abort(fiber.error)
return result
}
toString { "NamedTemporaryFile(%(name))" }
}
class TemporaryDirectory {
construct new() { init_("", TempFile.gettempprefix(), TempFile.gettempdir(), true) }
construct new(suffix) { init_(suffix, TempFile.gettempprefix(), TempFile.gettempdir(), true) }
construct new(suffix, prefix) { init_(suffix, prefix, TempFile.gettempdir(), true) }
construct new(suffix, prefix, dir) { init_(suffix, prefix, dir, true) }
construct new(suffix, prefix, dir, delete) { init_(suffix, prefix, dir, delete) }
init_(suffix, prefix, dir, delete) {
var d = dir != null ? dir : TempFile.gettempdir()
_name = TempFile.mkdtemp(suffix, prefix, d)
_delete = delete
_closed = false
}
name { _name }
path { Path.new(_name) }
delete { _delete }
delete=(value) { _delete = value }
closed { _closed }
cleanup() {
if (_closed) return
_closed = true
if (_delete) {
var fiber = Fiber.new { Path.new(_name).rmtree() }
fiber.try()
}
}
use(fn) {
var fiber = Fiber.new { fn.call(this) }
var result = fiber.try()
cleanup()
if (fiber.error != null) Fiber.abort(fiber.error)
return result
}
toString { "TemporaryDirectory(%(name))" }
}
+177
View File
@@ -0,0 +1,177 @@
// Please do not edit this file. It has been generated automatically
// from `src/module/tempfile.wren` using `util/wren_to_c_string.py`
static const char* tempfileModuleSource =
"// retoor <retoor@molodetz.nl>\n"
"\n"
"import \"io\" for Directory, File, FileFlags\n"
"import \"os\" for Platform\n"
"import \"env\" for Environment\n"
"import \"crypto\" for Crypto\n"
"import \"strutil\" for Str\n"
"import \"bytes\" for Bytes\n"
"import \"pathlib\" for Path\n"
"\n"
"class TempFile {\n"
" static tempdir { __tempdir }\n"
" static tempdir=(value) { __tempdir = value }\n"
"\n"
" static gettempdir() {\n"
" if (__tempdir != null) return __tempdir\n"
" var candidates = [\"TMPDIR\", \"TEMP\", \"TMP\"]\n"
" for (name in candidates) {\n"
" var value = Environment.get(name)\n"
" if (value != null && value.count > 0) return value\n"
" }\n"
" if (Platform.isPosix) return \"/tmp\"\n"
" return \"C:\\\\Temp\"\n"
" }\n"
"\n"
" static gettempprefix() { \"tmp\" }\n"
"\n"
" static randomSuffix_() {\n"
" var bytes = Crypto.randomBytes(4)\n"
" return Str.hexEncode(Bytes.fromList(bytes))\n"
" }\n"
"\n"
" static mktemp() { mktemp(\"\", gettempprefix(), gettempdir()) }\n"
" static mktemp(suffix) { mktemp(suffix, gettempprefix(), gettempdir()) }\n"
" static mktemp(suffix, prefix) { mktemp(suffix, prefix, gettempdir()) }\n"
"\n"
" static mktemp(suffix, prefix, dir) {\n"
" var name = prefix + randomSuffix_() + suffix\n"
" return dir + \"/\" + name\n"
" }\n"
"\n"
" static mkstemp() { mkstemp(\"\", gettempprefix(), gettempdir()) }\n"
" static mkstemp(suffix) { mkstemp(suffix, gettempprefix(), gettempdir()) }\n"
" static mkstemp(suffix, prefix) { mkstemp(suffix, prefix, gettempdir()) }\n"
"\n"
" static mkstemp(suffix, prefix, dir) {\n"
" var flags = FileFlags.writeOnly | FileFlags.create | FileFlags.exclusive\n"
" for (i in 0...100) {\n"
" var name = prefix + randomSuffix_() + suffix\n"
" var path = dir + \"/\" + name\n"
" var file\n"
" var error\n"
" var fiber = Fiber.new {\n"
" file = File.openWithFlags(path, flags)\n"
" }\n"
" fiber.try()\n"
" if (fiber.error == null && file != null) {\n"
" file.close()\n"
" return path\n"
" }\n"
" }\n"
" Fiber.abort(\"Failed to create temporary file after 100 attempts.\")\n"
" }\n"
"\n"
" static mkdtemp() { mkdtemp(\"\", gettempprefix(), gettempdir()) }\n"
" static mkdtemp(suffix) { mkdtemp(suffix, gettempprefix(), gettempdir()) }\n"
" static mkdtemp(suffix, prefix) { mkdtemp(suffix, prefix, gettempdir()) }\n"
"\n"
" static mkdtemp(suffix, prefix, dir) {\n"
" for (i in 0...100) {\n"
" var name = prefix + randomSuffix_() + suffix\n"
" var path = dir + \"/\" + name\n"
" var p = Path.new(path)\n"
" if (!p.exists()) {\n"
" Directory.create(path)\n"
" return path\n"
" }\n"
" }\n"
" Fiber.abort(\"Failed to create temporary directory after 100 attempts.\")\n"
" }\n"
"}\n"
"\n"
"class NamedTemporaryFile {\n"
" construct new() { init_(\"\", TempFile.gettempprefix(), TempFile.gettempdir(), true) }\n"
" construct new(suffix) { init_(suffix, TempFile.gettempprefix(), TempFile.gettempdir(), true) }\n"
" construct new(suffix, prefix) { init_(suffix, prefix, TempFile.gettempdir(), true) }\n"
" construct new(suffix, prefix, dir) { init_(suffix, prefix, dir, true) }\n"
" construct new(suffix, prefix, dir, delete) { init_(suffix, prefix, dir, delete) }\n"
"\n"
" init_(suffix, prefix, dir, delete) {\n"
" var d = dir != null ? dir : TempFile.gettempdir()\n"
" _name = TempFile.mkstemp(suffix, prefix, d)\n"
" _delete = delete\n"
" _closed = false\n"
" }\n"
"\n"
" name { _name }\n"
" path { Path.new(_name) }\n"
" delete { _delete }\n"
" delete=(value) { _delete = value }\n"
" closed { _closed }\n"
"\n"
" write(content) {\n"
" if (_closed) Fiber.abort(\"File is closed.\")\n"
" File.openWithFlags(_name, FileFlags.writeOnly | FileFlags.create) {|f|\n"
" f.writeBytes(content, 0)\n"
" }\n"
" }\n"
"\n"
" read() {\n"
" if (_closed) Fiber.abort(\"File is closed.\")\n"
" return File.read(_name)\n"
" }\n"
"\n"
" close() {\n"
" if (_closed) return\n"
" _closed = true\n"
" if (_delete) {\n"
" var fiber = Fiber.new { File.delete(_name) }\n"
" fiber.try()\n"
" }\n"
" }\n"
"\n"
" use(fn) {\n"
" var fiber = Fiber.new { fn.call(this) }\n"
" var result = fiber.try()\n"
" close()\n"
" if (fiber.error != null) Fiber.abort(fiber.error)\n"
" return result\n"
" }\n"
"\n"
" toString { \"NamedTemporaryFile(%(name))\" }\n"
"}\n"
"\n"
"class TemporaryDirectory {\n"
" construct new() { init_(\"\", TempFile.gettempprefix(), TempFile.gettempdir(), true) }\n"
" construct new(suffix) { init_(suffix, TempFile.gettempprefix(), TempFile.gettempdir(), true) }\n"
" construct new(suffix, prefix) { init_(suffix, prefix, TempFile.gettempdir(), true) }\n"
" construct new(suffix, prefix, dir) { init_(suffix, prefix, dir, true) }\n"
" construct new(suffix, prefix, dir, delete) { init_(suffix, prefix, dir, delete) }\n"
"\n"
" init_(suffix, prefix, dir, delete) {\n"
" var d = dir != null ? dir : TempFile.gettempdir()\n"
" _name = TempFile.mkdtemp(suffix, prefix, d)\n"
" _delete = delete\n"
" _closed = false\n"
" }\n"
"\n"
" name { _name }\n"
" path { Path.new(_name) }\n"
" delete { _delete }\n"
" delete=(value) { _delete = value }\n"
" closed { _closed }\n"
"\n"
" cleanup() {\n"
" if (_closed) return\n"
" _closed = true\n"
" if (_delete) {\n"
" var fiber = Fiber.new { Path.new(_name).rmtree() }\n"
" fiber.try()\n"
" }\n"
" }\n"
"\n"
" use(fn) {\n"
" var fiber = Fiber.new { fn.call(this) }\n"
" var result = fiber.try()\n"
" cleanup()\n"
" if (fiber.error != null) Fiber.abort(fiber.error)\n"
" return result\n"
" }\n"
"\n"
" toString { \"TemporaryDirectory(%(name))\" }\n"
"}\n";