diff --git a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpBackend.kt b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpBackend.kt index eddc926d..938e69e7 100644 --- a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpBackend.kt +++ b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpBackend.kt @@ -318,6 +318,8 @@ class CSharpBackend(setup: BackendSetup) : Backend private val stdLibraryResources: List = declareResources( base = dirPath("lang", "temper", "be", "csharp", "std"), + filePath("Io", "IoSupport.cs"), + filePath("Keyboard", "KeyboardSupport.cs"), filePath("Regex", "IntRangeSet.cs"), filePath("Regex", "RegexSupport.cs"), filePath("Temporal", "TemporalSupport.cs"), diff --git a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpSupportNetwork.kt b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpSupportNetwork.kt index f5e245e4..7ef7a869 100644 --- a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpSupportNetwork.kt +++ b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CSharpSupportNetwork.kt @@ -1105,6 +1105,21 @@ private val promiseBuilderGetPromise = PropertyAccess( "Task", ) +private val stdSleep = StaticCall( + "stdSleep", + StandardNames.temperStdIoStdSleep, +) + +private val stdReadLine = StaticCall( + "stdReadLine", + StandardNames.temperStdIoStdReadLine, +) + +private val stdNextKeypress = StaticCall( + "stdNextKeypress", + StandardNames.temperStdKeyboardStdNextKeypress, +) + private val stdNetSend = StaticCall( "stdNetSend", StandardNames.temperCoreNetCoreStdNetSend, @@ -1497,6 +1512,9 @@ private val connectedReferences = listOf( stringToInt, stringToInt64, stdNetSend, + stdSleep, + stdReadLine, + stdNextKeypress, ).flatMap { ref -> ref.connectedNames.map { it to ref } }.toMap() private val connectedTypes = mapOf) -> List)?>>( diff --git a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CsProj.kt b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CsProj.kt index 0d2c72b9..bcfa6bfc 100644 --- a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CsProj.kt +++ b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/CsProj.kt @@ -28,7 +28,7 @@ data class CsProj( attribute("Sdk", "Microsoft.NET.Sdk") "PropertyGroup" { // Currently, dotnet warns if you request net5.0 or earlier. - "TargetFramework" { -"net6.0" } + "TargetFramework" { -"net8.0" } // Core project info. outputType?.let { "OutputType" { -it } } rootNamespace?.let { "RootNamespace" { -it } } diff --git a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/StandardNames.kt b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/StandardNames.kt index bcfbd3a6..060b3a9c 100644 --- a/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/StandardNames.kt +++ b/be-csharp/src/commonMain/kotlin/lang/temper/be/csharp/StandardNames.kt @@ -172,6 +172,14 @@ object StandardNames { val temperStdTemporalTemporalSupportYearsBetween = temperStdTemporalTemporalSupport.member("YearsBetween") val temperStdTemporalTemporalSupportIsoWeekdayNum = temperStdTemporalTemporalSupport.member("IsoWeekdayNum") val temperStdTemporalTemporalSupportFromIsoString = temperStdTemporalTemporalSupport.member("FromIsoString") + private val temperStdIo = temperStd.space("Io") + private val temperStdIoIoSupport = temperStdIo.type("IoSupport") + val temperStdIoStdSleep = temperStdIoIoSupport.member("StdSleep") + val temperStdIoStdReadLine = temperStdIoIoSupport.member("StdReadLine") + private val temperStdKeyboard = temperStd.space("Keyboard") + private val temperStdKeyboardKeyboardSupport = temperStdKeyboard.type("KeyboardSupport") + val temperStdKeyboardStdNextKeypress = temperStdKeyboardKeyboardSupport.member("StdNextKeypress") + private val temperStdNet = temperStd.space("Net") val temperCoreNetINetResponse = temperStdNet.type("INetResponse") val temperCoreNetSupport = temperStdNet.type("NetSupport") diff --git a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/library-template/RootNamespaceSpot.csproj b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/library-template/RootNamespaceSpot.csproj index 36409e08..10c72f06 100644 --- a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/library-template/RootNamespaceSpot.csproj +++ b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/library-template/RootNamespaceSpot.csproj @@ -1,12 +1,12 @@ - net48;net6.0 + net48;net8.0 RootNamespaceSpot - + diff --git a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Io/IoSupport.cs b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Io/IoSupport.cs new file mode 100644 index 00000000..50d8ea78 --- /dev/null +++ b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Io/IoSupport.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace TemperLang.Std.Io +{ + public static class IoSupport + { + public static async Task> StdSleep(int ms) + { + await Task.Delay(ms); + return Tuple.Create(null); + } + + public static async Task StdReadLine() + { + return await Task.Run(() => Console.ReadLine()); + } + } +} diff --git a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Keyboard/KeyboardSupport.cs b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Keyboard/KeyboardSupport.cs new file mode 100644 index 00000000..2f8d40a8 --- /dev/null +++ b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Keyboard/KeyboardSupport.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; + +namespace TemperLang.Std.Keyboard +{ + public static class KeyboardSupport + { + public static async Task StdNextKeypress() + { + return await Task.Run(() => + { + if (Console.IsInputRedirected) return null; + var key = Console.ReadKey(true); + switch (key.Key) + { + case ConsoleKey.UpArrow: return "ArrowUp"; + case ConsoleKey.DownArrow: return "ArrowDown"; + case ConsoleKey.LeftArrow: return "ArrowLeft"; + case ConsoleKey.RightArrow: return "ArrowRight"; + case ConsoleKey.Escape: return "Escape"; + case ConsoleKey.Enter: return "Enter"; + case ConsoleKey.Backspace: return "Backspace"; + case ConsoleKey.Tab: return "Tab"; + default: return key.KeyChar.ToString(); + } + }); + } + } +} diff --git a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Regex/RegexSupport.cs b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Regex/RegexSupport.cs index 7967b2ca..a65f2b89 100644 --- a/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Regex/RegexSupport.cs +++ b/be-csharp/src/commonMain/resources/lang/temper/be/csharp/std/Regex/RegexSupport.cs @@ -151,7 +151,7 @@ RegexRefs regexRefs ); } var groupsDict = new ReadOnlyDictionaryWrapper( - new OrderedDictionary(resultGroups) + new TemperLang.Core.OrderedDictionary(resultGroups) ); var full = match.Groups[0].Captures[0]; var fullGroup = new Group("full", full.Value, full.Index, full.Index + full.Length); @@ -204,7 +204,7 @@ internal static IReadOnlyList CompiledSplit( RegexRefs regexRefs ) { - return ((R::Regex)compiled).Split(text).AsReadOnly(); + return TemperLang.Core.Listed.AsReadOnly(((R::Regex)compiled).Split(text)); } static IRegexNode IntRangeSetToUtf16CodePattern(List ranges) diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaSupportNetwork.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaSupportNetwork.kt index 40476d8e..3b890f20 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaSupportNetwork.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/JavaSupportNetwork.kt @@ -1148,6 +1148,13 @@ val JavaLang.doneResult by receiver { // Async support val JavaLang.runAsync by receiver { separateCode(temperRunAsync) } +// std/io support +val JavaLang.stdSleep by receiver { separateCode(temperStdSleep) } +val JavaLang.stdReadLine by receiver { separateCode(temperStdReadLine) } + +// std/keyboard support +val JavaLang.stdNextKeypress by receiver { separateCode(temperStdNextKeypress) } + // std/net support val JavaLang.netCoreStdNetSend by receiver { separateCode(temperNetCoreStdNetSend) } @@ -1590,4 +1597,7 @@ private val connections: Map SupportCode)> = mapOf( "empty" to { it.empty }, "ignore" to { it.doNothing }, "stdNetSend" to { it.netCoreStdNetSend }, + "stdSleep" to { it.stdSleep }, + "stdReadLine" to { it.stdReadLine }, + "stdNextKeypress" to { it.stdNextKeypress }, ) diff --git a/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt b/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt index ac88c62f..d7a70f8b 100644 --- a/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt +++ b/be-java/src/commonMain/kotlin/lang/temper/be/java/StandardNames.kt @@ -238,6 +238,13 @@ val temperRegexCompiledReplace = temperRegexCore.qualifyKnownSafe("regexCompiled val temperRegexCompiledSplit = temperRegexCore.qualifyKnownSafe("regexCompiledSplit") val temperRegexFormatterPushCodeTo = temperRegexCore.qualifyKnownSafe("regexFormatterPushCodeTo") +// std/io +val temperStdSleep = temperCore.qualifyKnownSafe("stdSleep") +val temperStdReadLine = temperCore.qualifyKnownSafe("stdReadLine") + +// std/keyboard +val temperStdNextKeypress = temperCore.qualifyKnownSafe("stdNextKeypress") + // std/net val temperNetPkg = temperPkg.qualifyKnownSafe("net") val temperNetCore = temperNetPkg.qualifyKnownSafe("Core") diff --git a/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java b/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java index 4d5dfcdd..b1be6c80 100644 --- a/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java +++ b/be-java/src/commonMain/resources/lang/temper/be/java/temper-core/src/main/java/temper/core/Core.java @@ -1924,10 +1924,141 @@ public static void runAsync(Supplier>> genera */ public static void waitUntilTasksComplete() { ForkJoinPool commonPool = ForkJoinPool.commonPool(); - // This timeout is sufficient for functional tests. - // If a long running main method needs more time, it should - // negotiate promises for termination with the tasks it spawns. - commonPool.awaitQuiescence(10L, TimeUnit.SECONDS); + // Wait until the pool is truly idle (all tasks complete). + while (!commonPool.isQuiescent()) { + commonPool.awaitQuiescence(60L, TimeUnit.SECONDS); + } + } + + // std/io support + + @SuppressWarnings("unchecked") + public static java.util.concurrent.CompletableFuture> stdSleep(int ms) { + java.util.concurrent.CompletableFuture> future = new java.util.concurrent.CompletableFuture<>(); + ForkJoinPool.commonPool().execute(() -> { + try { + Thread.sleep(ms); + future.complete(Optional.empty()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + future.completeExceptionally(e); + } + }); + return future; + } + + public static java.util.concurrent.CompletableFuture stdReadLine() { + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + ForkJoinPool.commonPool().execute(() -> { + try { + java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.InputStreamReader(System.in, java.nio.charset.StandardCharsets.UTF_8)); + String line = reader.readLine(); + future.complete(line); // null on EOF + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private static final boolean IS_WINDOWS = System.getProperty("os.name", "").toLowerCase().startsWith("win"); + + public static java.util.concurrent.CompletableFuture stdNextKeypress() { + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + ForkJoinPool.commonPool().execute(() -> { + try { + if (IS_WINDOWS) { + stdNextKeypressWindows(future); + } else { + stdNextKeypressUnix(future); + } + } catch (Exception e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private static void stdNextKeypressUnix(java.util.concurrent.CompletableFuture future) throws Exception { + if (System.console() == null) { + future.complete(null); + return; + } + String[] cmd = {"/bin/sh", "-c", "stty raw -echo future) throws Exception { + // Use PowerShell to read a single key without echo. + // Returns "VirtualKeyCode,KeyChar" e.g. "38,0" for ArrowUp or "65,a" for 'a'. + ProcessBuilder pb = new ProcessBuilder( + "powershell", "-NoProfile", "-Command", + "$k=[Console]::ReadKey($true); Write-Host \"$($k.Key),$($k.KeyChar)\"" + ); + pb.redirectErrorStream(true); + Process proc = pb.start(); + java.io.BufferedReader reader = new java.io.BufferedReader( + new java.io.InputStreamReader(proc.getInputStream(), java.nio.charset.StandardCharsets.UTF_8)); + String line = reader.readLine(); + proc.waitFor(); + if (line == null || line.isEmpty()) { + future.complete(null); + return; + } + int comma = line.indexOf(','); + if (comma < 0) { + future.complete(line); + return; + } + String vk = line.substring(0, comma).trim(); + String ch = line.substring(comma + 1).trim(); + switch (vk) { + case "UpArrow": future.complete("ArrowUp"); break; + case "DownArrow": future.complete("ArrowDown"); break; + case "LeftArrow": future.complete("ArrowLeft"); break; + case "RightArrow": future.complete("ArrowRight"); break; + case "Enter": future.complete("Enter"); break; + case "Escape": future.complete("Escape"); break; + case "Backspace": future.complete("Backspace"); break; + case "Tab": future.complete("Tab"); break; + case "Spacebar": future.complete(" "); break; + default: + // For regular characters, use the KeyChar value + if (ch.length() == 1 && ch.charAt(0) != 0) { + future.complete(ch); + } else { + future.complete(vk); + } + break; + } } } diff --git a/be-js/src/commonMain/kotlin/lang/temper/be/js/JsBackend.kt b/be-js/src/commonMain/kotlin/lang/temper/be/js/JsBackend.kt index 6df1c4ce..bca62d0e 100644 --- a/be-js/src/commonMain/kotlin/lang/temper/be/js/JsBackend.kt +++ b/be-js/src/commonMain/kotlin/lang/temper/be/js/JsBackend.kt @@ -619,6 +619,8 @@ class JsBackend private constructor( filePath("float.js"), filePath("int.js"), filePath("interface.js"), + filePath("io.js"), + filePath("keyboard.js"), filePath("listed.js"), filePath("mapped.js"), filePath("net.js"), diff --git a/be-js/src/commonMain/kotlin/lang/temper/be/js/JsSupportNetwork.kt b/be-js/src/commonMain/kotlin/lang/temper/be/js/JsSupportNetwork.kt index 5ec8d399..090eae42 100644 --- a/be-js/src/commonMain/kotlin/lang/temper/be/js/JsSupportNetwork.kt +++ b/be-js/src/commonMain/kotlin/lang/temper/be/js/JsSupportNetwork.kt @@ -465,6 +465,11 @@ private val supportedAutoConnecteds = setOf( "String::toInt32", "String::toInt64", "StringBuilder::appendCodePoint", + // std/io + "stdSleep", + "stdReadLine", + // std/keyboard + "stdNextKeypress", // std/net "stdNetSend", "NetResponse", diff --git a/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/index.js b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/index.js index b6db0af2..2d507424 100644 --- a/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/index.js +++ b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/index.js @@ -9,6 +9,7 @@ export * from "./int.js"; export * from "./interface.js"; export * from "./listed.js"; export * from "./mapped.js"; +export * from "./io.js"; export * from "./net.js"; export * from "./pair.js"; export * from "./regex.js"; diff --git a/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/io.js b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/io.js new file mode 100644 index 00000000..2101cd8d --- /dev/null +++ b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/io.js @@ -0,0 +1,30 @@ +import { empty } from "./core.js"; +import { createInterface } from "readline"; + +/** + * @param {number} ms + * @returns {Promise} + */ +export function stdSleep(ms) { + return new Promise(resolve => setTimeout(() => resolve(empty()), ms)); +} + +/** + * @returns {Promise} + */ +export function stdReadLine() { + return new Promise(resolve => { + if (typeof process !== 'undefined' && process.stdin) { + const rl = createInterface({ input: process.stdin }); + rl.once('line', line => { + rl.close(); + resolve(line); + }); + rl.once('close', () => { + resolve(null); // EOF + }); + } else { + resolve(null); + } + }); +} diff --git a/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/keyboard.js b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/keyboard.js new file mode 100644 index 00000000..23e028e4 --- /dev/null +++ b/be-js/src/commonMain/resources/lang/temper/be/js/temper-core/keyboard.js @@ -0,0 +1,26 @@ +/** + * @returns {Promise} + */ +export function stdNextKeypress() { + return new Promise(resolve => { + if (typeof process !== 'undefined' && process.stdin && process.stdin.isTTY) { + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + process.stdin.setRawMode(true); + process.stdin.once('data', data => { + process.stdin.setRawMode(false); + process.stdin.pause(); + const str = data.toString(); + if (str === '\x1b[A') resolve("ArrowUp"); + else if (str === '\x1b[B') resolve("ArrowDown"); + else if (str === '\x1b[C') resolve("ArrowRight"); + else if (str === '\x1b[D') resolve("ArrowLeft"); + else if (str === '\x1b') resolve("Escape"); + else if (str === '\r' || str === '\n') resolve("Enter"); + else resolve(str); + }); + } else { + resolve(null); + } + }); +} diff --git a/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaSupportNetwork.kt b/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaSupportNetwork.kt index f8400938..91815ce1 100644 --- a/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaSupportNetwork.kt +++ b/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaSupportNetwork.kt @@ -99,7 +99,7 @@ internal fun operatorToName( BuiltinOperatorId.Print -> "print" BuiltinOperatorId.StrCat -> "concat" BuiltinOperatorId.Listify -> "listof" - BuiltinOperatorId.Async -> "TODO" // TODO + BuiltinOperatorId.Async -> "async_launch" // should not be used with CoroutineStrategy.TranslateToGenerator BuiltinOperatorId.AdaptGeneratorFn, BuiltinOperatorId.SafeAdaptGeneratorFn, diff --git a/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaTranslator.kt b/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaTranslator.kt index a4e9d3ed..3b9d7762 100644 --- a/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaTranslator.kt +++ b/be-lua/src/commonMain/kotlin/lang/temper/be/lua/LuaTranslator.kt @@ -2444,6 +2444,22 @@ private class ModuleParts( addAll(preDecls) addAll(globalFuncs) addAll(topLevels) + // Run the cooperative scheduler if any async blocks were registered. + // This is a no-op if no coroutines were launched via temper.async_launch. + add( + Lua.CallStmt( + pos, + Lua.FunctionCallExpr( + pos, + Lua.DotIndexExpr( + pos, + Lua.Name(pos, name("temper")), + Lua.Name(pos, name("run_scheduler")), + ), + Lua.Args(pos, Lua.Exprs(pos, listOf())), + ), + ), + ) addAll(exports) }, Lua.ReturnStmt( diff --git a/be-lua/src/commonMain/resources/lang/temper/be/lua/temper-core/init.lua b/be-lua/src/commonMain/resources/lang/temper/be/lua/temper-core/init.lua index d624ddb4..02f82448 100644 --- a/be-lua/src/commonMain/resources/lang/temper/be/lua/temper-core/init.lua +++ b/be-lua/src/commonMain/resources/lang/temper/be/lua/temper-core/init.lua @@ -253,6 +253,181 @@ function temper.generator_next(f) return f() end +-- Cooperative coroutine scheduler for async blocks. +-- Lua is single-threaded, so we use non-blocking IO and round-robin scheduling. + +local PROMISE_RESOLVED = "resolved" +local PROMISE_SLEEP = "sleep" +local PROMISE_READLINE = "readline" +local PROMISE_KEYPRESS = "keypress" + +local scheduler_tasks = {} + +local function get_time() + local ok, socket = pcall(require, "socket") + if ok then return socket.gettime() end + return os.time() +end + +local function make_resolved(value) + return { state = PROMISE_RESOLVED, value = value, + await = function(self) return self.value end } +end + +local function make_sleep_promise(deadline) + return { state = PROMISE_SLEEP, deadline = deadline, value = nil, + await = function(self) return coro_yield(self) end } +end + +local function make_readline_promise() + return { state = PROMISE_READLINE, value = nil, + await = function(self) return coro_yield(self) end } +end + +local function make_keypress_promise() + return { state = PROMISE_KEYPRESS, value = nil, + await = function(self) return coro_yield(self) end } +end + +-- Register an async block for cooperative scheduling. +function temper.async_launch(generatorFactory) + local gen = generatorFactory() + -- Step once to start the coroutine + local ok, result = pcall(gen) + if ok and result ~= nil and type(result) == "table" and result.state ~= nil then + table.insert(scheduler_tasks, { step = gen, waiting_for = result }) + end + -- If coroutine finished immediately or errored, don't register +end + +-- Run the cooperative scheduler. No-op if no coroutines were registered. +function temper.run_scheduler() + if #scheduler_tasks == 0 then return end + + local function read_line() + local line = io.read("*l") + return line -- nil on EOF + end + + -- Terminal state management for keypress polling + local old_stty = nil + local has_keypress_tasks = false + for _, task in ipairs(scheduler_tasks) do + if task.waiting_for and task.waiting_for.state == PROMISE_KEYPRESS then + has_keypress_tasks = true + break + end + end + if has_keypress_tasks then + local save_handle = io.popen("stty -g 2>/dev/null", "r") + if save_handle then + old_stty = save_handle:read("*l") + save_handle:close() + if old_stty == "" then old_stty = nil end + end + end + + local function restore_terminal() + if old_stty then os.execute("stty " .. old_stty .. " 2>/dev/null") end + end + + local function poll_char() + os.execute("stty raw -echo -icanon min 0 time 0 2>/dev/null") + local ch = io.read(1) + if old_stty then os.execute("stty " .. old_stty .. " 2>/dev/null") end + return ch + end + + local function parse_keypress(ch) + if ch:byte() == 27 then + local next = poll_char() + if next and next == '[' then + local arrow = poll_char() + local arrows = {A='ArrowUp', B='ArrowDown', C='ArrowRight', D='ArrowLeft'} + return arrows[arrow] or 'Escape' + else + return 'Escape' + end + elseif ch == '\r' or ch == '\n' then + return 'Enter' + else + return ch + end + end + + -- Main scheduler loop + while #scheduler_tasks > 0 do + local now = get_time() + local any_progressed = false + local all_input = true + + local i = 1 + while i <= #scheduler_tasks do + local task = scheduler_tasks[i] + local promise = task.waiting_for + local should_resume = false + local resume_value = nil + + if promise == nil then + table.remove(scheduler_tasks, i) + elseif promise.state == PROMISE_RESOLVED then + should_resume = true + resume_value = promise.value + all_input = false + elseif promise.state == PROMISE_SLEEP then + all_input = false + if now >= promise.deadline then + should_resume = true + resume_value = nil + else + i = i + 1 + end + elseif promise.state == PROMISE_READLINE then + local line = read_line() + should_resume = true + resume_value = line -- nil on EOF + elseif promise.state == PROMISE_KEYPRESS then + local ch = poll_char() + if ch ~= nil then + should_resume = true + resume_value = parse_keypress(ch) + else + i = i + 1 + end + else + i = i + 1 + end + + if should_resume then + any_progressed = true + local ok, result = pcall(task.step, resume_value) + if ok and result ~= nil and type(result) == "table" and result.state ~= nil then + task.waiting_for = result + i = i + 1 + else + table.remove(scheduler_tasks, i) + end + end + end + + -- If only input tasks remain and nothing is progressing, exit + if all_input and #scheduler_tasks > 0 then + break + end + + -- Avoid busy-spinning when nothing progressed + if not any_progressed and #scheduler_tasks > 0 then + local ok, socket = pcall(require, "socket") + if ok then socket.sleep(0.01) else os.execute("sleep 0.01") end + end + end + + restore_terminal() +end + +-- Keep legacy temper.TODO as alias for backward compatibility +temper.TODO = temper.async_launch + do local inst_meta = { __index = function(self, k) @@ -2049,4 +2224,23 @@ do end end +-- std/io support +-- Uses the scheduler's promise types when inside coroutines, +-- falls back to blocking when called outside coroutines. + +function temper.stdsleep(ms) + local deadline = get_time() + (ms / 1000) + return make_sleep_promise(deadline) +end + +function temper.stdreadline() + return make_readline_promise() +end + +-- std/keyboard support + +function temper.stdnextkeypress() + return make_keypress_promise() +end + return temper diff --git a/be-py/src/commonMain/kotlin/lang/temper/be/py/PySupportNetwork.kt b/be-py/src/commonMain/kotlin/lang/temper/be/py/PySupportNetwork.kt index 9879e031..8f174ff9 100644 --- a/be-py/src/commonMain/kotlin/lang/temper/be/py/PySupportNetwork.kt +++ b/be-py/src/commonMain/kotlin/lang/temper/be/py/PySupportNetwork.kt @@ -908,6 +908,9 @@ val NetResponseGetStatus = val NetResponseGetContentType = inlineAttribute("NetResponse::getContentType", PyIdentifierName("content_type")) val NetResponseGetBodyContent = inlineAttribute("NetResponse::getBodyContent", PyIdentifierName("text")) val StdNetSend = PySeparateCode("std_net_send", RUNTIME) +val StdSleep = PySeparateCode("std_sleep", RUNTIME) +val StdReadLine = PySeparateCode("std_read_line", RUNTIME) +val StdNextKeypress = PySeparateCode("std_next_keypress", RUNTIME) val mathInf = PySeparateCode("inf", MATH) val mathNan = PySeparateCode("nan", MATH) @@ -1213,4 +1216,7 @@ private val pyConnections = mapOf( "empty" to EmptyInliner, "ignore" to Ignore, "stdNetSend" to StdNetSend, + "stdSleep" to StdSleep, + "stdReadLine" to StdReadLine, + "stdNextKeypress" to StdNextKeypress, ) diff --git a/be-py/src/commonMain/resources/lang/temper/be/py/temper-core/temper_core/__init__.py b/be-py/src/commonMain/resources/lang/temper/be/py/temper-core/temper_core/__init__.py index 4b06d515..d60d355a 100644 --- a/be-py/src/commonMain/resources/lang/temper/be/py/temper-core/temper_core/__init__.py +++ b/be-py/src/commonMain/resources/lang/temper/be/py/temper-core/temper_core/__init__.py @@ -1399,3 +1399,91 @@ def _utf8_byte_of(code_point: int, byte_offset: int, n_bytes: int) -> int: def _utf16_size(char: str) -> int: return 1 + (ord(char) >= 0x10000) + + +# std/io support + +import time as _time + + +def std_sleep(ms: int) -> 'Future[None]': + """Sleep for ms milliseconds, returning a Future.""" + f: Future[None] = new_unbound_promise() + + def _do_sleep(): + _time.sleep(ms / 1000.0) + f.set_result(None) + + _executor.submit(_do_sleep) + return f + + +def std_read_line() -> 'Future[Optional[str]]': + """Read a line from stdin, returning a Future.""" + import sys as _sys + f: 'Future[Optional[str]]' = new_unbound_promise() + + def _do_read(): + try: + line = _sys.stdin.readline() + if line == '': + f.set_result(None) # EOF + else: + f.set_result(line.rstrip('\n').rstrip('\r')) + except EOFError: + f.set_result(None) + + _executor.submit(_do_read) + return f + + +def std_next_keypress() -> 'Future[Optional[str]]': + """Wait for next keypress, returning a Future.""" + import sys as _sys + f: 'Future[Optional[str]]' = new_unbound_promise() + + def _do_read(): + try: + if _sys.platform == 'win32': + import msvcrt as _msvcrt + ch = _msvcrt.getwch() + if ch in ('\x00', '\xe0'): + ch2 = _msvcrt.getwch() + specials = {'H': 'ArrowUp', 'P': 'ArrowDown', 'K': 'ArrowLeft', 'M': 'ArrowRight'} + f.set_result(specials.get(ch2, ch2)) + elif ch == '\r': + f.set_result('Enter') + elif ch == '\x1b': + f.set_result('Escape') + else: + f.set_result(ch) + elif _sys.stdin.isatty(): + import tty as _tty, termios as _termios + fd = _sys.stdin.fileno() + old_settings = _termios.tcgetattr(fd) + try: + _tty.setraw(fd) + ch = _sys.stdin.read(1) + if ch == '\x1b': + next_ch = _sys.stdin.read(1) + if next_ch == '[': + arrow = _sys.stdin.read(1) + arrows = {'A': 'ArrowUp', 'B': 'ArrowDown', 'C': 'ArrowRight', 'D': 'ArrowLeft'} + f.set_result(arrows.get(arrow, 'Escape')) + else: + f.set_result('Escape') + elif ch == '\r' or ch == '\n': + f.set_result('Enter') + elif ch == '': + f.set_result(None) + else: + f.set_result(ch) + finally: + _termios.tcsetattr(fd, _termios.TCSADRAIN, old_settings) + else: + f.set_result(None) + except EOFError: + f.set_result(None) + + _executor.submit(_do_read) + return f diff --git a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustBackend.kt b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustBackend.kt index f5c46ff3..ef5a2c15 100644 --- a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustBackend.kt +++ b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustBackend.kt @@ -85,12 +85,14 @@ class RustBackend(setup: BackendSetup) : Backend(Facto val deps = mutableSetOf() val featuresByDep = mutableMapOf>() val modNames = mutableListOf() + val allUsedSupportPaths = mutableSetOf() for (module in modules) { // Actually translate. val modKids = allModKids[module.codeLocation.codeLocation.sourceFile] ?: listOf() val translator = RustTranslator(dependenciesBuilder, module, names, modKids) val mod = translator.translateModule() add(mod) + allUsedSupportPaths.addAll(translator.usedSupportFunctionPaths) // We slap mod.rs on the end of each, so exclude that. val segments = mod.path.dirName().segments val pathed = segments.subListToEnd(1).joinToString("::") { it.fullName.escapeIfNeeded() } @@ -122,6 +124,32 @@ class RustBackend(setup: BackendSetup) : Backend(Facto ).let { deps.add(it) } } } + // Detect temper-std dependency from connected function paths (e.g. "temper_std::io::std_sleep"). + // Connected functions bypass the import system, so they aren't caught by the import scan above. + val stdCrateName = STD_ROOT_PACKAGE_NAME.replace("-", "_") + val usedStdModules = allUsedSupportPaths + .filter { it.startsWith("$stdCrateName::") } + .mapNotNull { it.split("::").getOrNull(1) } + .filter { it in stdFeatures } + .toSet() + if (usedStdModules.isNotEmpty()) { + val stdConfig = libraryConfigurations.byLibraryName.entries + .firstOrNull { it.key.text == STANDARD_LIBRARY_NAME } + if (stdConfig != null) { + val stdNaming = names.packageNamingsByRoot[stdConfig.value.libraryRoot] + if (stdNaming != null) { + Dep( + libraryName = STANDARD_LIBRARY_NAME, + naming = stdNaming, + path = "../$STANDARD_LIBRARY_NAME", + version = stdConfig.value.versionOrDefault(), + ).let { deps.add(it) } + for (mod in usedStdModules) { + featuresByDep.computeIfAbsent(STANDARD_LIBRARY_NAME) { mutableSetOf() }.add(mod) + } + } + } + } // Merge lib. TODO Can we sort init in dependency order? Do we need to? Dep order maybe not hierarchical? linkLayers(finished.pos, allModPaths, allModKids) addLib(finished.pos, modules, allModKids, deps, libraryConfiguration, modNames) @@ -152,9 +180,12 @@ class RustBackend(setup: BackendSetup) : Backend(Facto append("regex = { version = \"=1.12.2\", optional = true }\n") append("time = { version = \"=0.3.41\", optional = true }\n") append("ureq = { version = \"=3.1.2\", optional = true }\n") + append("crossterm = { version = \"=0.28.1\", optional = true }\n") // Below aren't dependencies section anymore, but eh. append("\n") append("[features]\n") + append("io = []\n") + append("keyboard = [\"crossterm\"]\n") append("net = [\"ureq\"]\n") // Implied: append("regex = [\"regex\"]\n") append("temporal = [\"time\"]\n") @@ -225,7 +256,7 @@ class RustBackend(setup: BackendSetup) : Backend(Facto private val resourceBase = dirPath("lang", "temper", "be", "rust") private val coreResourceBase = resourceBase.resolveDir("temper-core") private val stdResourceBase = resourceBase.resolveDir("std") - val stdSupportNeeders = setOf("net", "regex", "temporal") + val stdSupportNeeders = setOf("io", "keyboard", "net", "regex", "temporal") val stdFeatures = stdSupportNeeders // same set today but maybe not guaranteed private val templateResourceBase = resourceBase.resolveDir("library-template") diff --git a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustSupportNetwork.kt b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustSupportNetwork.kt index 64f9665e..ad60c5c5 100644 --- a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustSupportNetwork.kt +++ b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustSupportNetwork.kt @@ -914,6 +914,9 @@ private val neGeneric = EqNeGeneric("NeGeneric", BuiltinOperatorId.NeGeneric, Ru private val neIntInt = Infix("NeIntInt", BuiltinOperatorId.NeIntInt, RustOperator.NotEquals) private val neStrStr = CmpStrStr("NeStrStr", BuiltinOperatorId.NeStrStr, RustOperator.NotEquals) private val netSend = FunctionCall("stdNetSend", "send_request", cloneEvenIfFirst = true) +private val stdSleep = FunctionCall("stdSleep", "temper_std::io::std_sleep") +private val stdReadLine = FunctionCall("stdReadLine", "temper_std::io::std_read_line") +private val stdNextKeypress = FunctionCall("stdNextKeypress", "temper_std::keyboard::std_next_keypress") internal object PairConstructor : RustInlineSupportCode( "Pair::constructor", @@ -1139,6 +1142,9 @@ private val connectedReferences = listOf( mappedToMapBuilder, mappedValues, netSend, + stdSleep, + stdReadLine, + stdNextKeypress, promiseBuilderComplete, PairConstructor, regexCompileFormatted, diff --git a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustTranslator.kt b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustTranslator.kt index 6e6d975b..91811a54 100644 --- a/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustTranslator.kt +++ b/be-rust/src/commonMain/kotlin/lang/temper/be/rust/RustTranslator.kt @@ -94,6 +94,9 @@ class RustTranslator( DescriptorsForDeclarations.Key(RustBackend.Factory), )?.nameToDescriptor ?: mapOf() private var closureCount = 0 + + /** Tracks function paths referenced via connected support code (e.g. "temper_std::io::std_sleep"). */ + val usedSupportFunctionPaths = mutableSetOf() private val decls = mutableMapOf() private var insideMutableType = false private val failVars = mutableSetOf() @@ -1673,6 +1676,10 @@ class RustTranslator( call: TmpL.CallExpression, supportCode: RustInlineSupportCode, ): Rust.Expr { + // Track external crate references from connected functions. + if (supportCode is FunctionCall) { + usedSupportFunctionPaths.add(supportCode.functionName) + } val wantUnstrung = supportCode is ConsoleLog || supportCode is StrCat var first = true return supportCode.inlineToTree( diff --git a/be-rust/src/commonMain/resources/lang/temper/be/rust/std/io/support.rs b/be-rust/src/commonMain/resources/lang/temper/be/rust/std/io/support.rs new file mode 100644 index 00000000..71829476 --- /dev/null +++ b/be-rust/src/commonMain/resources/lang/temper/be/rust/std/io/support.rs @@ -0,0 +1,39 @@ +use std::sync::Arc; +use std::io::BufRead; +use temper_core::{Promise, PromiseBuilder, SafeGenerator}; + +pub fn std_sleep(ms: i32) -> Promise<()> { + let pb = PromiseBuilder::new(); + let promise = pb.promise(); + crate::run_async(Arc::new(move || { + let pb = pb.clone(); + SafeGenerator::from_fn(Arc::new(move |_generator: SafeGenerator<()>| { + std::thread::sleep(std::time::Duration::from_millis(ms as u64)); + pb.complete(()); + None + })) + })); + promise +} + +pub fn std_read_line() -> Promise>> { + let pb = PromiseBuilder::new(); + let promise = pb.promise(); + crate::run_async(Arc::new(move || { + let pb = pb.clone(); + SafeGenerator::from_fn(Arc::new(move |_generator: SafeGenerator<()>| { + let stdin = std::io::stdin(); + let mut line = String::new(); + match stdin.lock().read_line(&mut line) { + Ok(0) => pb.complete(None), + Ok(_) => { + let trimmed = line.trim_end_matches('\n').trim_end_matches('\r'); + pb.complete(Some(Arc::new(trimmed.to_string()))); + } + Err(_) => pb.complete(None), + } + None + })) + })); + promise +} diff --git a/be-rust/src/commonMain/resources/lang/temper/be/rust/std/keyboard/support.rs b/be-rust/src/commonMain/resources/lang/temper/be/rust/std/keyboard/support.rs new file mode 100644 index 00000000..080aae32 --- /dev/null +++ b/be-rust/src/commonMain/resources/lang/temper/be/rust/std/keyboard/support.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; +use temper_core::{Promise, PromiseBuilder, SafeGenerator}; + +#[cfg(not(feature = "keyboard"))] +pub fn std_next_keypress() -> Promise>> { + panic!() +} + +#[cfg(feature = "keyboard")] +pub fn std_next_keypress() -> Promise>> { + let pb = PromiseBuilder::new(); + let promise = pb.promise(); + crate::run_async(Arc::new(move || { + let pb = pb.clone(); + SafeGenerator::from_fn(Arc::new(move |_generator: SafeGenerator<()>| { + use crossterm::terminal; + use crossterm::event::{self, Event, KeyCode, KeyEvent}; + + terminal::enable_raw_mode().ok(); + let result = loop { + match event::read() { + Ok(Event::Key(KeyEvent { code, .. })) => { + break match code { + KeyCode::Up => Some("ArrowUp"), + KeyCode::Down => Some("ArrowDown"), + KeyCode::Left => Some("ArrowLeft"), + KeyCode::Right => Some("ArrowRight"), + KeyCode::Enter => Some("Enter"), + KeyCode::Esc => Some("Escape"), + KeyCode::Backspace => Some("Backspace"), + KeyCode::Tab => Some("Tab"), + KeyCode::Char(c) => { + terminal::disable_raw_mode().ok(); + pb.complete(Some(Arc::new(c.to_string()))); + return None; + } + _ => Some("Unknown"), + }; + } + Err(_) => { break None; } + _ => continue, + } + }; + terminal::disable_raw_mode().ok(); + pb.complete(result.map(|s| Arc::new(s.to_string()))); + None + })) + })); + promise +} diff --git a/be/src/commonMain/kotlin/lang/temper/be/README.md b/be/src/commonMain/kotlin/lang/temper/be/README.md index c0f3e5e1..01d1209a 100644 --- a/be/src/commonMain/kotlin/lang/temper/be/README.md +++ b/be/src/commonMain/kotlin/lang/temper/be/README.md @@ -227,4 +227,6 @@ to implement: - `Test::messages` - `Test::passing` - `stdNetSend` +- `stdReadLine` +- `stdSleep` diff --git a/frontend/src/commonMain/resources/std/config.temper.md b/frontend/src/commonMain/resources/std/config.temper.md index ee883778..976926cd 100644 --- a/frontend/src/commonMain/resources/std/config.temper.md +++ b/frontend/src/commonMain/resources/std/config.temper.md @@ -23,6 +23,7 @@ We might break these out into separate libraries in the future. import("./temporal"); import("./json"); import("./net"); + import("./io"); ## C# diff --git a/frontend/src/commonMain/resources/std/io/io.temper.md b/frontend/src/commonMain/resources/std/io/io.temper.md new file mode 100644 index 00000000..d6f73497 --- /dev/null +++ b/frontend/src/commonMain/resources/std/io/io.temper.md @@ -0,0 +1,22 @@ +# IO + +Basic input/output operations for interactive programs. + +## Sleep + +Pause execution for the given number of milliseconds. + + @connected("stdSleep") + export let sleep(ms: Int): Promise { + panic() + } + +## Read Line + +Read one line from standard input. Returns null on EOF. + + @connected("stdReadLine") + export let readLine(): Promise { + panic() + } + diff --git a/frontend/src/commonMain/resources/std/keyboard/keyboard.temper.md b/frontend/src/commonMain/resources/std/keyboard/keyboard.temper.md new file mode 100644 index 00000000..8cc02427 --- /dev/null +++ b/frontend/src/commonMain/resources/std/keyboard/keyboard.temper.md @@ -0,0 +1,13 @@ +# Keyboard + +Keyboard input for interactive programs. + +## Next Keypress + +Wait for and return the next keypress. Returns the key name as a string +(e.g. "a", "ArrowUp", "Enter", "Escape"). Returns null on EOF. + + @connected("stdNextKeypress") + export let nextKeypress(): Promise { + panic() + } diff --git a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt index db113e39..cdbb6b23 100644 --- a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt +++ b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTestSuiteI.kt @@ -148,6 +148,10 @@ interface FunctionalTestSuiteI { fun controlFlowIfReturn() = runFunctionalTest(Ft.ControlFlowIfReturn) + @Test + fun controlFlowIoSleep() = + runFunctionalTest(Ft.ControlFlowIoSleep) + @Test fun controlFlowLoopReenterable() = runFunctionalTest(Ft.ControlFlowLoopReenterable) diff --git a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt index ab8c0085..fac7a2b1 100644 --- a/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt +++ b/functional-test-suite/src/commonMain/kotlin/lang/temper/tests/FunctionalTests.kt @@ -53,6 +53,7 @@ enum class FunctionalTests(val test: FunctionalTestBase) { ControlFlowAsync(markdown("control-flow/async/async.temper.md", "ControlFlowAsync")), ControlFlowBubble(markdown("control-flow/bubble/bubble.temper.md", "ControlFlowBubble")), ControlFlowIfReturn(markdown("control-flow/if-return/if-return.temper.md", "ControlFlowIfReturn")), + ControlFlowIoSleep(markdown("control-flow/io-sleep/io-sleep.temper.md", "ControlFlowIoSleep")), ControlFlowLoopReenterable(markdown("control-flow/loop-reenterable/loop-reenterable.temper.md", "ControlFlowLoopReenterable")), ControlFlowLoops(markdown("control-flow/loops/loops.temper.md", "ControlFlowLoops")), FunctionsAsValues(markdown("functions/as-values/as-values.temper.md", "FunctionsAsValues")), diff --git a/functional-test-suite/src/commonMain/resources/config.temper.md b/functional-test-suite/src/commonMain/resources/config.temper.md index e4f1730a..19c850cc 100644 --- a/functional-test-suite/src/commonMain/resources/config.temper.md +++ b/functional-test-suite/src/commonMain/resources/config.temper.md @@ -26,6 +26,7 @@ Autogenerated file, to update use `./gradlew kcodegen:up`, to update this header import("./control-flow/async"); import("./control-flow/bubble"); import("./control-flow/if-return"); + import("./control-flow/io-sleep"); import("./control-flow/loop-reenterable"); import("./control-flow/loops"); import("./functions/as-values"); diff --git a/functional-test-suite/src/commonMain/resources/control-flow/io-sleep/io-sleep.temper.md b/functional-test-suite/src/commonMain/resources/control-flow/io-sleep/io-sleep.temper.md new file mode 100644 index 00000000..c8d43f32 --- /dev/null +++ b/functional-test-suite/src/commonMain/resources/control-flow/io-sleep/io-sleep.temper.md @@ -0,0 +1,74 @@ +# IO Sleep Functional Test + +This tests the `sleep()` function from `std/io`. + + let {sleep} = import("std/io"); + +The test runs inside an async block since `sleep` returns a `Promise`. + + async { (): GeneratorResult extends GeneratorFn => + +## Sleep returns and execution continues + +We verify that `sleep` completes (resolves its promise) and execution +continues after `await`. We use a short delay to avoid slowing the +test suite. + + do { + console.log("before sleep"); + await sleep(10); + console.log("after sleep"); + } orelse panic(); + +```log +before sleep +after sleep +``` + +## Multiple sleeps in sequence + + do { + console.log("a"); + await sleep(10); + console.log("b"); + await sleep(10); + console.log("c"); + } orelse panic(); + +```log +a +b +c +``` + +## Sleep with zero milliseconds + +A zero-ms sleep should resolve immediately. + + do { + console.log("before zero"); + await sleep(0); + console.log("after zero"); + } orelse panic(); + +```log +before zero +after zero +``` + +## Sleep interleaved with computation + + do { + var sum = 0; + for (var i = 0; i < 3; ++i) { + sum = sum + i; + await sleep(5); + } + console.log("sum: ${sum.toString()}"); + } orelse panic(); + +```log +sum: 3 +``` + + } // ends async {...}