diff --git a/daslib/apply.das b/daslib/apply.das index 7cf34d9c8b..feaca0fc76 100644 --- a/daslib/apply.das +++ b/daslib/apply.das @@ -244,16 +244,20 @@ def clone_block_with_name(name : string; blk : ExpressionPtr; hasExtraArg : bool [call_macro(name="apply")] // apply(value, block) class ApplyMacro : AstCallMacro { - //! This macro implements the apply() pattern. The idea is that for each entry in the structure, variant, or tuple, - //! the block will be invoked. Both element name, and element value are passed to the block. - //! For example + //! This macro implements the ``apply()`` pattern. For each field in a structure, variant, or tuple, + //! the block is invoked with the field name and value. An optional third block argument receives + //! per-field annotations as ``array>``. //! - //! struct Bar + //! .. code-block:: das + //! + //! struct Bar { //! x, y : float - //! apply([[Bar x=1.,y=2.]]) <| $ ( name:string; field ) + //! } + //! apply(Bar(x=1.0, y=2.0)) $(name, field) { //! print("{name} = {field} ") + //! } //! - //! Would print x = 1.0 y = 2.0 + //! Would print ``x = 1 y = 2`` def override visit(prog : ProgramPtr; mod : Module?; var expr : smart_ptr) : ExpressionPtr { macro_verify(expr.arguments |> length == 2, prog, expr.at, "expecting apply(value, block)") if (expr.arguments[0]._type != null) {// need value inferred diff --git a/daslib/async_boost.das b/daslib/async_boost.das index bff6d3cd11..010f887ec2 100644 --- a/daslib/async_boost.das +++ b/daslib/async_boost.das @@ -379,3 +379,35 @@ def public async_run_all(var a : array>) { } } } + + +def public async_timeout(var a : iterator; max_frames : int) : bool { + //! This function runs an async function for at most `max_frames` frames. + //! Returns ``true`` if the async function completed within the limit, + //! ``false`` if it was terminated due to timeout. + var frames = 0 + for (t in a) { + frames ++ + if (frames >= max_frames) { + return false + } + } + return true +} + + +def public async_race(var a : iterator; var b : iterator) : int { + //! This function runs two async functions concurrently and returns the + //! index (0 or 1) of whichever finishes first. The other is abandoned. + var ta : bool + var tb : bool + while (true) { + if (!next(a, ta) || empty(a)) { + return 0 + } + if (!next(b, tb) || empty(b)) { + return 1 + } + } + return -1 +} diff --git a/daslib/coroutines.das b/daslib/coroutines.das index 258a053a52..e9ecf289c5 100644 --- a/daslib/coroutines.das +++ b/daslib/coroutines.das @@ -26,6 +26,22 @@ typedef Coroutine = iterator //! An array of coroutines. typedef Coroutines = array +[macro_function] +def private verify_inside_coroutine(prog : ProgramPtr; call : smart_ptr&) : bool { + //! Verifies that a call macro is being used inside a [coroutine] or [async] annotated function. + //! Returns true if valid, false if not (and emits macro_error). + if (call.inFunction == null) { + return true + } + for (ann in call.inFunction.annotations) { + if (ann.annotation.name == "coroutine" || ann.annotation.name == "async") { + return true + } + } + macro_error(prog, call.at, "{call.name}() can only be used inside a [coroutine] or [async] function") + return false +} + [call_macro(name="yeild_from")] class private YieldFrom : AstCallMacro { //! This macro converts yield_from(THAT) expression into:: @@ -55,9 +71,10 @@ class private CoContinue : AstCallMacro { //! That way co_continue() does not distract from the fact that it is a generator. def override visit(prog : ProgramPtr; mod : Module?; var call : smart_ptr) : ExpressionPtr { //! Visits the co_continue macro call and rewrites it to ``yield true``. - // TODO: verify if we are in coroutine - // TODO: verify if we are in coroutine which returns bool macro_verify(call.arguments |> length == 0, prog, call.at, "expecting co_continue()") + if (!verify_inside_coroutine(prog, call)) { + return <- default + } return <- qmacro_expr() { yield true } @@ -74,8 +91,10 @@ class private CoAwait : AstCallMacro { //! The idea is that coroutine or generator can wait for a sub-coroutine to finish. def override visit(prog : ProgramPtr; mod : Module?; var call : smart_ptr) : ExpressionPtr { //! Visits the co_await macro call and rewrites it into a for-yield loop over the sub-coroutine. - // TODO: verify if we are calling co_await on a coroutine. macro_verify(call.arguments |> length == 1, prog, call.at, "expecting co_await(subroutine)") + if (!verify_inside_coroutine(prog, call)) { + return <- default + } let iname = make_unique_private_name("_co_await_iterator", call.at) return <- qmacro_block() { for ($i(iname) in $e(call.arguments[0])) { diff --git a/doc/source/reference/tutorials.rst b/doc/source/reference/tutorials.rst index e21c878676..1b87aea304 100644 --- a/doc/source/reference/tutorials.rst +++ b/doc/source/reference/tutorials.rst @@ -75,6 +75,8 @@ introduced in earlier tutorials. tutorials/45_debug_agents.rst tutorials/46_apply_in_context.rst tutorials/47_data_walker.rst + tutorials/48_apply.rst + tutorials/49_async.rst .. _tutorials_integration_c: diff --git a/doc/source/reference/tutorials/44_compile_and_run.rst b/doc/source/reference/tutorials/44_compile_and_run.rst index add72bf15a..77cebb3390 100644 --- a/doc/source/reference/tutorials/44_compile_and_run.rst +++ b/doc/source/reference/tutorials/44_compile_and_run.rst @@ -11,8 +11,8 @@ Compiling and Running Programs at Runtime single: Tutorial; Multiple Contexts single: Tutorial; Virtual File System -This tutorial covers **compiling and running daScript programs from -daScript** — dynamically compiling source code at runtime, simulating +This tutorial covers **compiling and running daslang programs from +daslang** — dynamically compiling source code at runtime, simulating it into a runnable context, and invoking exported functions across context boundaries. @@ -32,7 +32,7 @@ section. Compile from string =================== -The simplest way to compile daScript at runtime is ``compile``, which +The simplest way to compile daslang at runtime is ``compile``, which takes a module name, source text, and ``CodeOfPolicies``. The callback receives ``(ok : bool, program : smart_ptr, issues : string)``. diff --git a/doc/source/reference/tutorials/47_data_walker.rst b/doc/source/reference/tutorials/47_data_walker.rst index 2c3eced358..b8bd6f4acc 100644 --- a/doc/source/reference/tutorials/47_data_walker.rst +++ b/doc/source/reference/tutorials/47_data_walker.rst @@ -11,11 +11,11 @@ Data Walking with DapiDataWalker single: Tutorial; Runtime Introspection This tutorial covers ``DapiDataWalker`` — a visitor-pattern class for -inspecting and transforming daScript values at runtime. You subclass +inspecting and transforming daslang values at runtime. You subclass it, override callbacks for the types you care about, and walk any data through RTTI type information. -Prerequisites: basic daScript knowledge (structs, arrays, tables, +Prerequisites: basic daslang knowledge (structs, arrays, tables, enumerations, bitfields). .. code-block:: das @@ -249,7 +249,7 @@ containing value names. Bitfields trigger ``Bitfield`` with a JSON serializer — putting it all together =========================================== -A practical walker that serializes any daScript value to JSON. +A practical walker that serializes any daslang value to JSON. This combines structure, array, table, tuple, and scalar callbacks into one coherent class. The walker writes to a ``StringBuilderWriter`` for efficiency — no intermediate string concatenation: @@ -426,3 +426,5 @@ Quick reference ``VarInfo``, ``EnumInfo``). Previous tutorial: :ref:`tutorial_apply_in_context` + + Next tutorial: :ref:`tutorial_apply` diff --git a/doc/source/reference/tutorials/48_apply.rst b/doc/source/reference/tutorials/48_apply.rst new file mode 100644 index 0000000000..ca47e7d584 --- /dev/null +++ b/doc/source/reference/tutorials/48_apply.rst @@ -0,0 +1,255 @@ +.. _tutorial_apply: + +========================================== +Compile-Time Field Iteration with apply +========================================== + +.. index:: + single: Tutorial; apply + single: Tutorial; Compile-Time Iteration + single: Tutorial; Struct Fields + single: Tutorial; Field Annotations + +This tutorial covers ``daslib/apply`` — a call macro that iterates +struct, tuple, and variant fields at compile time. Unlike runtime +RTTI walkers, ``apply`` generates specialized code per field with +zero reflection overhead. + +Prerequisites: basic daslang knowledge (structs, tuples, variants). + +.. code-block:: das + + options gen2 + options rtti + + require rtti + require daslib/apply + require daslib/strings_boost + + +Basic struct iteration +======================= + +``apply(value) $(name, field) { ... }`` visits every field of a struct. +``name`` is a compile-time string constant with the field name, and +``field`` is the field value with its concrete type: + +.. code-block:: das + + struct Hero { + name : string + health : int + speed : float + } + + let hero = Hero(name = "Archer", health = 100, speed = 3.5) + + apply(hero) $(name, field) { + print(" {name} = {field}\n") + } + +Output:: + + name = Archer + health = 100 + speed = 3.5 + + +Compile-time dispatch with static_if +====================================== + +Because ``name`` is known at compile time, ``static_if`` can branch +on it. Only the matching branch compiles for each field — the others +are discarded entirely: + +.. code-block:: das + + struct Config { + width : int + height : int + title : string + fullscreen : bool + } + + let cfg = Config(width = 1920, height = 1080, + title = "My Game", fullscreen = true) + + apply(cfg) $(name, field) { + static_if (name == "title") { + print(" Title (special handling): \"{field}\"\n") + } else { + print(" {name} = {field}\n") + } + } + +You can also dispatch on the **type** with ``typeinfo stripped_typename(field)``. + + +Mutating fields +================ + +Pass a ``var`` (mutable) variable and ``apply`` gives mutable +references to each field: + +.. code-block:: das + + struct Stats { + attack : int + defense : int + magic : int + } + + var stats = Stats(attack = 10, defense = 5, magic = 8) + + apply(stats) $(name, field) { + field *= 2 + } + // stats is now Stats(attack=20, defense=10, magic=16) + + +Tuples +======= + +``apply`` works on tuples too. Unnamed tuple fields are named +``_0``, ``_1``, etc. Named tuples use their declared names: + +.. code-block:: das + + let pair : tuple = (42, "hello") + apply(pair) $(name, field) { + print(" {name} = {field}\n") + } + // _0 = 42 + // _1 = hello + + let point : tuple = (1.0, 2.0) + apply(point) $(name, field) { + print(" {name} = {field}\n") + } + // x = 1 + // y = 2 + + +Variants +========= + +For variants, ``apply`` visits **only the currently active +alternative**. The block fires for the one alternative that is set: + +.. code-block:: das + + variant Shape { + circle : float + rect : float2 + triangle : float3 + } + + let s = Shape(circle = 5.0) + apply(s) $(name, field) { + print(" Shape is {name}: {field}\n") + } + // Shape is circle: 5 + + +Generic describe function +========================== + +``apply`` is ideal for building generic utilities that work on any +struct without knowing its fields in advance: + +.. code-block:: das + + def describe(value) { + var first = true + print("\{") + apply(value) $(name, field) { + if (!first) { + print(", ") + } + first = false + static_if (typeinfo stripped_typename(field) == "string") { + print("{name}=\"{field}\"") + } else { + print("{name}={field}") + } + } + print("\}") + } + +This prints any struct in ``{field=value, ...}`` format without +writing type-specific code. + + +Field annotations (3-argument form) +===================================== + +Struct fields can carry metadata via ``@`` annotations: + +.. code-block:: das + + struct DbRecord { + @column="user_name" name : string + @column="user_email" email : string + @skip id : int + @column="age" age : int + } + +Annotation syntax: + +- ``@name`` — boolean (defaults to ``true``) +- ``@name=value`` — integer, float, or bare identifier (string) +- ``@name="text"`` — quoted string + +The 3-argument form ``apply(value) $(name, field, annotations)`` +receives annotations as ``array>`` +for each field. ``RttiValue`` is a variant with alternatives +``tBool``, ``tInt``, ``tFloat``, ``tString``, etc. + +.. code-block:: das + + apply(record) $(name : string; field; annotations) { + var column_name = name + var skip = false + for (ann in annotations) { + if (ann.name == "skip") { + skip = true + } elif (ann.name == "column") { + column_name = ann.data as tString + } + } + if (!skip) { + // use column_name and field ... + } + } + +This pattern powers ``daslib/json_boost``'s ``@rename``, +``@optional``, ``@enum_as_int``, ``@unescape``, and ``@embed`` +field annotations. + + +Full source +============ + +The complete tutorial source is in +``tutorials/language/48_apply.das``. + +Run it with:: + + daslang.exe tutorials/language/48_apply.das + + +.. seealso:: + + Full source: :download:`tutorials/language/48_apply.das <../../../../tutorials/language/48_apply.das>` + + :ref:`stdlib_apply` — ``apply`` call macro reference. + + :ref:`stdlib_rtti` — ``RttiValue`` variant type used by the + 3-argument annotation form. + + :ref:`tutorial_data_walker` — runtime data walking with + ``DapiDataWalker`` (Tutorial 47). + + Previous tutorial: :ref:`tutorial_data_walker` + + Next tutorial: :ref:`tutorial_async` diff --git a/doc/source/reference/tutorials/49_async.rst b/doc/source/reference/tutorials/49_async.rst new file mode 100644 index 0000000000..34c764e43b --- /dev/null +++ b/doc/source/reference/tutorials/49_async.rst @@ -0,0 +1,300 @@ +.. _tutorial_async: + +========================================== +Async / Await +========================================== + +.. index:: + single: Tutorial; Async + single: Tutorial; Await + single: Tutorial; Async Generators + single: Tutorial; Cooperative Multitasking + +This tutorial covers ``daslib/async_boost`` — an async/await framework +built on top of daslang generators. Every ``[async]`` function is +transformed at compile time into a state-machine generator. No threads, +channels, or job queues are involved — everything is single-threaded +cooperative multitasking. + +Prerequisites: Tutorial 15 (Iterators and Generators), +Tutorial 40 (Coroutines). + +.. code-block:: das + + options gen2 + options no_unused_function_arguments = false + + require daslib/async_boost + require daslib/coroutines + + +Void async — the simplest form +================================ + +An ``[async]`` function with ``: void`` return type becomes a +``generator`` state machine, just like a ``[coroutine]``. +Call ``await_next_frame()`` to suspend until the next step: + +.. code-block:: das + + [async] + def greet(name : string) : void { + print(" hello, ") + await_next_frame() + print("{name}!\n") + } + + def demo_void_async() { + print("=== void async ===\n") + var it <- greet("world") + var step = 1 + for (running in it) { + print(" -- step {step} --\n") + step ++ + } + print(" -- done --\n") + } + +Each iteration of the ``for`` loop advances the generator by one +step. The function body resumes after the last ``await_next_frame()`` +and runs until the next one (or until it returns). + + +Typed async — yielding values +=============================== + +An ``[async]`` function with a non-void return type yields values +wrapped in ``variant``. Each ``await_next_frame()`` +yields ``variant(wait=true)``; each ``yield value`` yields +``variant(res=value)``: + +.. code-block:: das + + [async] + def compute(x : int) : int { + await_next_frame() // simulate one frame of work + yield x * 2 + } + + def demo_typed_async() { + for (v in compute(21)) { + if (v is wait) { + print(" (waiting...)\n") + } elif (v is res) { + print(" result = {v as res}\n") // 42 + } + } + } + +The consumer checks ``v is wait`` vs ``v is res`` to distinguish +suspension frames from actual results. + + +Await — waiting for an async result +===================================== + +Inside an ``[async]`` function you can ``await`` another async call. +``await`` suspends the parent until the child completes and extracts +the result: + +.. code-block:: das + + [async] + def add_one(x : int) : int { + await_next_frame() + yield x + 1 + } + + [async] + def chained_math() : void { + var a = 0 + a = await <| add_one(0) // copy-assign: a = 1 + a <- await <| add_one(a) // move-assign: a = 2 + let b <- await <| add_one(a) // let-bind: b = 3 + print(" a={a}, b={b}\n") + } + +Three forms of ``await``: + +- ``a = await <| fn(args)`` — copy-assign the result +- ``a <- await <| fn(args)`` — move-assign the result +- ``let b <- await <| fn(args)`` — bind to a new variable + + +Struct return with move semantics +================================== + +Async functions can yield structs. Use ``yield <-`` to move the +result out (useful for non-copyable data): + +.. code-block:: das + + struct Measurement { + sensor_id : int + value : float + tag : string + } + + [async] + def read_sensor(id : int) : Measurement { + await_next_frame() + var m : Measurement + m.sensor_id = id + m.value = 3.14 + m.tag = "temperature" + yield <- m + } + + [async] + def process_sensors() : void { + let m <- await <| read_sensor(1) + print(" sensor {m.sensor_id}: {m.value} ({m.tag})\n") + } + + +Iterating async generators +============================ + +A typed async function that yields multiple values acts as an +asynchronous generator. Consumers iterate and check ``v is res`` +to extract each value: + +.. code-block:: das + + [async] + def fibonacci_async(n : int) : int { + var a = 0 + var b = 1 + for (i in range(n)) { + yield a + let next_val = a + b + a = b + b = next_val + await_next_frame() + } + } + + def demo_async_iteration() { + print(" fibonacci: ") + for (v in fibonacci_async(8)) { + if (v is res) { + print("{v as res} ") + } + } + print("\n") + } + +Output: ``0 1 1 2 3 5 8 13`` + + +Running tasks +============== + +``async_boost`` provides four task runners: + +- ``async_run(it)`` — drive a single task to completion +- ``async_run_all(tasks)`` — drive all tasks cooperatively, + round-robin one step per task per frame +- ``async_timeout(it, max_frames)`` — drive a task for at most + *max_frames* steps; returns ``true`` if it completed in time +- ``async_race(a, b)`` — drive two tasks cooperatively; returns + ``0`` if *a* finishes first, ``1`` if *b* finishes first + +.. code-block:: das + + [async] + def worker(name : string; frames : int) : void { + for (i in range(frames)) { + print(" {name}: frame {i + 1}/{frames}\n") + await_next_frame() + } + } + +``async_run`` steps through a single task: + +.. code-block:: das + + var single <- worker("solo", 3) + async_run(single) + +``async_run_all`` interleaves multiple tasks: + +.. code-block:: das + + var tasks : array> + tasks |> emplace <| worker("alpha", 2) + tasks |> emplace <| worker("beta", 3) + async_run_all(tasks) + +``async_timeout`` enforces a deadline: + +.. code-block:: das + + var fast <- worker("fast", 2) + let completed = async_timeout(fast, 10) // true + var slow <- worker("slow", 100) + let timed_out = async_timeout(slow, 3) // false (timeout) + +``async_race`` runs two tasks and returns which finishes first: + +.. code-block:: das + + var racer_a <- worker("A", 2) + var racer_b <- worker("B", 5) + let winner = async_race(racer_a, racer_b) // 0 (A wins) + + +Mixing async with coroutines +============================== + +An ``[async]`` function can ``await`` a ``[coroutine]``. This lets +you compose low-level coroutine logic with high-level async +orchestration: + +.. code-block:: das + + [coroutine] + def tick_counter(n : int) { + for (i in range(n)) { + print(" tick {i + 1}\n") + co_continue() + } + } + + [async] + def orchestrator() : void { + print(" orchestrator: start\n") + await_next_frame() + print(" orchestrator: awaiting coroutine\n") + await <| tick_counter(3) + print(" orchestrator: coroutine done\n") + await_next_frame() + print(" orchestrator: finish\n") + } + + +Full source +============ + +The complete tutorial source is in +``tutorials/language/49_async.das``. + +Run it with:: + + daslang.exe tutorials/language/49_async.das + + +.. seealso:: + + Full source: :download:`tutorials/language/49_async.das <../../../../tutorials/language/49_async.das>` + + :ref:`stdlib_async_boost` — ``async_boost`` module reference. + + :ref:`stdlib_coroutines` — coroutines module reference + (underlying generator framework). + + :ref:`tutorial_coroutines` — Tutorial 40: Coroutines + (prerequisite). + + Previous tutorial: :ref:`tutorial_apply` + diff --git a/doc/source/reference/tutorials/integration_c_10_threading.rst b/doc/source/reference/tutorials/integration_c_10_threading.rst index fd2703cc67..22357ed28e 100644 --- a/doc/source/reference/tutorials/integration_c_10_threading.rst +++ b/doc/source/reference/tutorials/integration_c_10_threading.rst @@ -153,7 +153,7 @@ Key points: Part B — Compile on a worker thread ==================================== -Create a fully independent daScript environment on a new thread. +Create a fully independent daslang environment on a new thread. .. code-block:: c diff --git a/doc/source/reference/tutorials/integration_cpp_21_threading.rst b/doc/source/reference/tutorials/integration_cpp_21_threading.rst index 323770e1f1..ff41fae706 100644 --- a/doc/source/reference/tutorials/integration_cpp_21_threading.rst +++ b/doc/source/reference/tutorials/integration_cpp_21_threading.rst @@ -137,7 +137,7 @@ Key points: Part B — Compile on a worker thread ==================================== -Create a fully independent daScript environment on a new thread. +Create a fully independent daslang environment on a new thread. This is the pattern used in ``test_threads.cpp`` for concurrent compilation benchmarks. diff --git a/doc/source/reference/tutorials/macros/03_function_macro.rst b/doc/source/reference/tutorials/macros/03_function_macro.rst index 74a191f60f..d22634e1e7 100644 --- a/doc/source/reference/tutorials/macros/03_function_macro.rst +++ b/doc/source/reference/tutorials/macros/03_function_macro.rst @@ -362,7 +362,7 @@ Key details: * **``ExprConstInt``** — one of the ``ExprConst*`` family of AST nodes (``ExprConstFloat``, ``ExprConstString``, ``ExprConstBool``, etc.). All have a ``.value`` field of the corresponding type. -* **``is`` / ``as``** — daScript’s type-test and downcast operators work +* **``is`` / ``as``** — daslang's type-test and downcast operators work on AST node types just like on classes. * **``[macro_function]``** — marks the function as available during compilation (macro expansion time). Without this annotation, the @@ -505,8 +505,8 @@ The check ``expr.func._module.name == "$"`` distinguishes the builtin ``print``: * **``_module``** — the field name uses an underscore prefix because - ``module`` is a reserved keyword in daScript. In C++ the field is - ``Function::module``; in daScript macros it is ``func._module``. + ``module`` is a reserved keyword in daslang. In C++ the field is + ``Function::module``; in daslang macros it is ``func._module``. * **``"$"``** — the builtin module name. All built-in functions (``print``, ``assert``, ``length``, math functions, etc.) belong to module ``"$"``. @@ -536,7 +536,7 @@ The ``lint()`` method creates the visitor, adapts it with return true } -* **``make_visitor(*astVisitor)``** — wraps the daScript visitor object +* **``make_visitor(*astVisitor)``** — wraps the daslang visitor object into an adapter that the C++ ``visit()`` function can call. The ``*`` dereferences the smart pointer. * **``visit(func, adapter)``** — walks the function’s AST, calling diff --git a/doc/source/reference/tutorials/macros/11_reader_macro.rst b/doc/source/reference/tutorials/macros/11_reader_macro.rst index 685fb301e2..1391b13b6b 100644 --- a/doc/source/reference/tutorials/macros/11_reader_macro.rst +++ b/doc/source/reference/tutorials/macros/11_reader_macro.rst @@ -12,7 +12,7 @@ Previous tutorials intercepted calls, functions, structures, blocks, variants, for-loops, and lambda captures. Reader macros go further — -they embed **entirely custom syntax** inside daScript source code. +they embed **entirely custom syntax** inside daslang source code. ``[reader_macro(name="X")]`` registers a class that extends ``AstReaderMacro``. Reader macros are invoked with the syntax @@ -31,9 +31,9 @@ they embed **entirely custom syntax** inside daScript source code. ``suffix(prog, mod, expr, info, outLine&, outFile?&) → string`` Called immediately **after** ``accept()`` during parsing. Returns - a string of daScript source code that is injected back into the + a string of daslang source code that is injected back into the parser's input stream. This is the **suffix pattern** — used when - the macro appears at module level and generates top-level daScript + the macro appears at module level and generates top-level daslang declarations (functions, structs, etc.). The ``ExprReader`` node is discarded. @@ -47,7 +47,7 @@ they embed **entirely custom syntax** inside daScript source code. - **Suffix pattern**: the reader macro appears at **module level** as a standalone statement (not assigned to a variable). ``suffix()`` - returns daScript source text that the parser re-parses. ``visit()`` + returns daslang source text that the parser re-parses. ``visit()`` is never called because the parser discards the ``ExprReader`` node. All reader macros share the same ``accept()`` idiom for collecting @@ -61,14 +61,14 @@ Motivation Embedding domain-specific notations — CSV data, regular expressions, JSON literals, template engines — is a common need. Reader macros let you write these in their native syntax and transform them at compile time -into efficient daScript code, without runtime parsing overhead. +into efficient daslang code, without runtime parsing overhead. This tutorial builds both patterns: - ``%csv~`` — a **visit** reader macro that parses CSV text at compile time into a string array (constant embedded in the AST) - ``%basic~`` — a **suffix** reader macro that transpiles a toy BASIC - program into a daScript function definition + program into a daslang function definition .. note:: @@ -131,7 +131,7 @@ to embed the resulting string array in the AST: return <- convert_to_expression(items, expr.at) } -``convert_to_expression`` takes any daScript value and converts it into +``convert_to_expression`` takes any daslang value and converts it into AST nodes — here turning an ``array`` into the equivalent of an array literal. This is the same utility used by ``daslib/json_boost`` to embed parsed JSON and by ``daslib/regex_boost`` to embed compiled @@ -142,7 +142,7 @@ BasicReader — suffix pattern ``BasicReader`` is registered with ``[reader_macro(name=basic)]``. It overrides ``suffix()`` instead of ``visit()``. The method parses a -tiny BASIC dialect and returns the equivalent daScript source code: +tiny BASIC dialect and returns the equivalent daslang source code: .. code-block:: das @@ -184,7 +184,7 @@ tiny BASIC dialect and returns the equivalent daScript source code: return result } -The returned string is valid gen2 daScript code. The parser receives +The returned string is valid gen2 daslang code. The parser receives this text and parses it as a normal function definition at module level. .. note:: @@ -268,7 +268,7 @@ Visit vs Suffix +------------------+--------------------------+---------------------------+ | When called | Type inference | Parsing (after accept) | +------------------+--------------------------+---------------------------+ -| Returns | AST expression | daScript source text | +| Returns | AST expression | daslang source text | +------------------+--------------------------+---------------------------+ | Usage context | Expression position | Module level | +------------------+--------------------------+---------------------------+ @@ -294,7 +294,7 @@ The standard library includes several reader macros: Usage: ``%stringify~ text %%`` - ``daslib/spoof.das`` — ``SpoofTemplateReader`` (visit) + ``SpoofInstanceReader`` (suffix): a template engine that stores - templates as strings and instantiates them by generating daScript + templates as strings and instantiates them by generating daslang source code at parse time. diff --git a/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst index 81257c45c4..e0f9711f0f 100644 --- a/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst +++ b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst @@ -60,7 +60,7 @@ The ``typeinfo`` keyword already provides many built-in traits: compile-time introspection — a description string for serialization, an array of enum names for UI dropdowns, or a boolean check for conditional compilation. ``[typeinfo_macro]`` lets you add such traits using pure -daScript code. +daslang code. The module file diff --git a/doc/source/reference/tutorials/macros/13_enumeration_macro.rst b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst index 8aa9829606..0a24374912 100644 --- a/doc/source/reference/tutorials/macros/13_enumeration_macro.rst +++ b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst @@ -31,7 +31,7 @@ only one method: Motivation ========== -Enumerations in daScript are simple value lists. But real-world code +Enumerations in daslang are simple value lists. But real-world code often needs derived information: - **A "total" sentinel** — the number of values, for array sizing or diff --git a/doc/source/stdlib/apply.rst b/doc/source/stdlib/apply.rst index 63b2a79678..092371f073 100644 --- a/doc/source/stdlib/apply.rst +++ b/doc/source/stdlib/apply.rst @@ -42,6 +42,12 @@ Example: // b = 3.14 // c = hello +.. seealso:: + + :ref:`tutorial_apply` — comprehensive tutorial covering structs, + tuples, variants, ``static_if`` dispatch, mutation, generic + ``describe``, and the 3-argument annotation form. + +++++++++++ @@ -52,16 +58,20 @@ Call macros .. das:attribute:: apply -This macro implements the apply() pattern. The idea is that for each entry in the structure, variant, or tuple, -the block will be invoked. Both element name, and element value are passed to the block. -For example +This macro implements the ``apply()`` pattern. For each field in a structure, variant, or tuple, +the block is invoked with the field name and value. An optional third block argument receives +per-field annotations as ``array>``. + +.. code-block:: das - struct Bar + struct Bar { x, y : float - apply([[Bar x=1.,y=2.]]) <| $ ( name:string; field ) + } + apply(Bar(x=1.0, y=2.0)) $(name, field) { print("{name} = {field} ") + } -Would print x = 1.0 y = 2.0 +Would print ``x = 1 y = 2`` diff --git a/doc/source/stdlib/ast.rst b/doc/source/stdlib/ast.rst index 5c6328e75f..aaf4e85328 100644 --- a/doc/source/stdlib/ast.rst +++ b/doc/source/stdlib/ast.rst @@ -5327,27 +5327,27 @@ Compilation time only expression which holds temporary information for the `AstR .. das:attribute:: ExprCallMacro -Compilation time only expression which holds temporary information for the `AstCallMacro`. +:Fields: * **at** : :ref:`LineInfo ` - Compilation time only expression which holds temporary information for the `AstCallMacro`. -:Fields: * **at** : :ref:`LineInfo ` - Location of the expression in source code + * **_type** : smart_ptr< :ref:`TypeDecl `> - Location of the expression in source code - * **_type** : smart_ptr< :ref:`TypeDecl `> - Type of the expression + * **__rtti** : string - Type of the expression - * **__rtti** : string - Runtime type information of the class of the expression (i.e "ExprConstant", "ExprCall", etc) + * **genFlags** : :ref:`ExprGenFlags ` - Runtime type information of the class of the expression (i.e "ExprConstant", "ExprCall", etc) - * **genFlags** : :ref:`ExprGenFlags ` - Expression generation flags + * **flags** : :ref:`ExprFlags ` - Expression generation flags - * **flags** : :ref:`ExprFlags ` - Expression flags + * **printFlags** : :ref:`ExprPrintFlags ` - Expression flags - * **printFlags** : :ref:`ExprPrintFlags ` - Expression print flags + * **name** : :ref:`das_string ` - Expression print flags - * **name** : :ref:`das_string ` - Name of the macro being called + * **arguments** : vector> - Name of the macro being called - * **arguments** : vector> - List of argument expressions + * **argumentsFailedToInfer** : bool - List of argument expressions - * **argumentsFailedToInfer** : bool - If the arguments failed to infer their types + * **atEnclosure** : :ref:`LineInfo ` - If the arguments failed to infer their types - * **atEnclosure** : :ref:`LineInfo ` - Location of the expression in source code + * **inFunction** : :ref:`Function `? - Location of the expression in source code * **macro** : :ref:`CallMacro `? - Call macro, if resolved diff --git a/doc/source/stdlib/async_boost.rst b/doc/source/stdlib/async_boost.rst index e01d7921c6..332280c1d8 100644 --- a/doc/source/stdlib/async_boost.rst +++ b/doc/source/stdlib/async_boost.rst @@ -8,9 +8,11 @@ Async/await coroutine macros .. das:module:: async_boost The ASYNC_BOOST module implements an async/await pattern for daslang using -channels and coroutines. It provides ``async`` for launching concurrent tasks -and ``await`` for waiting on their results, built on top of the job queue -infrastructure. +generator-based cooperative multitasking. It provides the ``[async]`` function +annotation, ``await`` for waiting on results, and ``await_next_frame`` for +suspending until the next step. Under the hood every ``[async]`` function is +transformed into a state-machine generator — no threads, channels, or job +queues are involved. All functions and symbols are in "async_boost" module, use require to get access to it. @@ -18,6 +20,12 @@ All functions and symbols are in "async_boost" module, use require to get access require daslib/async_boost +.. seealso:: + + :ref:`tutorial_async` — Tutorial 49: Async / Await. + + :ref:`stdlib_coroutines` — coroutines module (underlying generator framework). + ++++++++++++++++++++ @@ -115,3 +123,33 @@ This function runs all async function until they are finished (in parallel, star :Arguments: * **a** : array> ++++++++++++++ +Uncategorized ++++++++++++++ + +.. _function-async_boost_async_timeout_iterator_ls_auto_gr__int: + +.. das:function:: async_timeout(a: iterator; max_frames: int) : bool + +This function runs an async function for at most `max_frames` frames. +Returns ``true`` if the async function completed within the limit, +``false`` if it was terminated due to timeout. + + +:Arguments: * **a** : iterator + + * **max_frames** : int + +.. _function-async_boost_async_race_iterator_ls_auto_gr__iterator_ls_auto_gr_: + +.. das:function:: async_race(a: iterator; b: iterator) : int + +This function runs two async functions concurrently and returns the +index (0 or 1) of whichever finishes first. The other is abandoned. + + +:Arguments: * **a** : iterator + + * **b** : iterator + + diff --git a/doc/source/stdlib/debugapi.rst b/doc/source/stdlib/debugapi.rst index a5ffe5b124..a96fe6eafd 100644 --- a/doc/source/stdlib/debugapi.rst +++ b/doc/source/stdlib/debugapi.rst @@ -174,7 +174,7 @@ Classes .. das:attribute:: DapiDataWalker -Base class for walking daScript data structures. Subclass and override per-type visitor methods (`Int`, `Float`, `String`, `Array`, `Table`, etc.) to inspect values. Create with `make_data_walker`. +Base class for walking daslang data structures. Subclass and override per-type visitor methods (`Int`, `Float`, `String`, `Array`, `Table`, etc.) to inspect values. Create with `make_data_walker`. @@ -973,7 +973,7 @@ walk_data .. das:function:: walk_data(walker: smart_ptr; data: void?; struct_info: StructInfo) -Walks a daScript data structure using the provided `DataWalker`. The walker receives typed callbacks for each value encountered. Overloaded for raw data+`StructInfo`, `float4`+`TypeInfo`, and `void?`+`TypeInfo`. +Walks a daslang data structure using the provided `DataWalker`. The walker receives typed callbacks for each value encountered. Overloaded for raw data+`StructInfo`, `float4`+`TypeInfo`, and `void?`+`TypeInfo`. :Arguments: * **walker** : smart_ptr< :ref:`DataWalker `> implicit diff --git a/doc/source/stdlib/detail/async_boost.rst b/doc/source/stdlib/detail/async_boost.rst index 1f537a5add..0375e1cbef 100644 --- a/doc/source/stdlib/detail/async_boost.rst +++ b/doc/source/stdlib/detail/async_boost.rst @@ -40,3 +40,7 @@ .. |detail/function-async_boost-async_run_all| replace:: to be documented in |detail/function-async_boost-async_run_all|.rst +.. |detail/function-async_boost-async_timeout| replace:: to be documented in |detail/function-async_boost-async_timeout|.rst + +.. |detail/function-async_boost-async_race| replace:: to be documented in |detail/function-async_boost-async_race|.rst + diff --git a/doc/source/stdlib/detail/function-async_boost-async_race-0x576e990c9a340053.rst b/doc/source/stdlib/detail/function-async_boost-async_race-0x576e990c9a340053.rst new file mode 100644 index 0000000000..0d1eb4980e --- /dev/null +++ b/doc/source/stdlib/detail/function-async_boost-async_race-0x576e990c9a340053.rst @@ -0,0 +1,2 @@ +This function runs two async functions concurrently and returns the +index (0 or 1) of whichever finishes first. The other is abandoned. diff --git a/doc/source/stdlib/detail/function-async_boost-async_timeout-0x90c6a8e18a67722c.rst b/doc/source/stdlib/detail/function-async_boost-async_timeout-0x90c6a8e18a67722c.rst new file mode 100644 index 0000000000..2a95f34559 --- /dev/null +++ b/doc/source/stdlib/detail/function-async_boost-async_timeout-0x90c6a8e18a67722c.rst @@ -0,0 +1,3 @@ +This function runs an async function for at most `max_frames` frames. +Returns ``true`` if the async function completed within the limit, +``false`` if it was terminated due to timeout. diff --git a/doc/source/stdlib/detail/function-coroutines-verify_inside_coroutine-0xf135462c5b5a595a.rst b/doc/source/stdlib/detail/function-coroutines-verify_inside_coroutine-0xf135462c5b5a595a.rst new file mode 100644 index 0000000000..8c2a19337d --- /dev/null +++ b/doc/source/stdlib/detail/function-coroutines-verify_inside_coroutine-0xf135462c5b5a595a.rst @@ -0,0 +1,2 @@ +Verifies that a call macro is being used inside a [coroutine] or [async] annotated function. +Returns true if valid, false if not (and emits macro_error). diff --git a/doc/source/stdlib/detail/function_annotation-apply-apply.rst b/doc/source/stdlib/detail/function_annotation-apply-apply.rst index 4281f9f3e5..cb6615a6d8 100644 --- a/doc/source/stdlib/detail/function_annotation-apply-apply.rst +++ b/doc/source/stdlib/detail/function_annotation-apply-apply.rst @@ -1,10 +1,14 @@ -This macro implements the apply() pattern. The idea is that for each entry in the structure, variant, or tuple, -the block will be invoked. Both element name, and element value are passed to the block. -For example +This macro implements the ``apply()`` pattern. For each field in a structure, variant, or tuple, +the block is invoked with the field name and value. An optional third block argument receives +per-field annotations as ``array>``. - struct Bar +.. code-block:: das + + struct Bar { x, y : float - apply([[Bar x=1.,y=2.]]) <| $ ( name:string; field ) + } + apply(Bar(x=1.0, y=2.0)) $(name, field) { print("{name} = {field} ") + } -Would print x = 1.0 y = 2.0 +Would print ``x = 1 y = 2`` diff --git a/doc/source/stdlib/handmade/class-debugapi-DapiDataWalker.rst b/doc/source/stdlib/handmade/class-debugapi-DapiDataWalker.rst index 1bbe8b09c8..fa040009ac 100644 --- a/doc/source/stdlib/handmade/class-debugapi-DapiDataWalker.rst +++ b/doc/source/stdlib/handmade/class-debugapi-DapiDataWalker.rst @@ -1 +1 @@ -Base class for walking daScript data structures. Subclass and override per-type visitor methods (`Int`, `Float`, `String`, `Array`, `Table`, etc.) to inspect values. Create with `make_data_walker`. +Base class for walking daslang data structures. Subclass and override per-type visitor methods (`Int`, `Float`, `String`, `Array`, `Table`, etc.) to inspect values. Create with `make_data_walker`. diff --git a/doc/source/stdlib/handmade/function-debugapi-walk_data-0x92c8091c1cfaa8f1.rst b/doc/source/stdlib/handmade/function-debugapi-walk_data-0x92c8091c1cfaa8f1.rst index a8ae57b437..d85d760ca6 100644 --- a/doc/source/stdlib/handmade/function-debugapi-walk_data-0x92c8091c1cfaa8f1.rst +++ b/doc/source/stdlib/handmade/function-debugapi-walk_data-0x92c8091c1cfaa8f1.rst @@ -1 +1 @@ -Walks a daScript data structure using the provided `DataWalker`. The walker receives typed callbacks for each value encountered. Overloaded for raw data+`StructInfo`, `float4`+`TypeInfo`, and `void?`+`TypeInfo`. +Walks a daslang data structure using the provided `DataWalker`. The walker receives typed callbacks for each value encountered. Overloaded for raw data+`StructInfo`, `float4`+`TypeInfo`, and `void?`+`TypeInfo`. diff --git a/doc/source/stdlib/handmade/module-apply.rst b/doc/source/stdlib/handmade/module-apply.rst index c59b225808..acb5978187 100644 --- a/doc/source/stdlib/handmade/module-apply.rst +++ b/doc/source/stdlib/handmade/module-apply.rst @@ -32,3 +32,9 @@ Example: // a = 42 // b = 3.14 // c = hello + +.. seealso:: + + :ref:`tutorial_apply` — comprehensive tutorial covering structs, + tuples, variants, ``static_if`` dispatch, mutation, generic + ``describe``, and the 3-argument annotation form. diff --git a/doc/source/stdlib/handmade/module-async_boost.rst b/doc/source/stdlib/handmade/module-async_boost.rst index cdedea7b1a..db2e3735ca 100644 --- a/doc/source/stdlib/handmade/module-async_boost.rst +++ b/doc/source/stdlib/handmade/module-async_boost.rst @@ -1,10 +1,18 @@ The ASYNC_BOOST module implements an async/await pattern for daslang using -channels and coroutines. It provides ``async`` for launching concurrent tasks -and ``await`` for waiting on their results, built on top of the job queue -infrastructure. +generator-based cooperative multitasking. It provides the ``[async]`` function +annotation, ``await`` for waiting on results, and ``await_next_frame`` for +suspending until the next step. Under the hood every ``[async]`` function is +transformed into a state-machine generator — no threads, channels, or job +queues are involved. All functions and symbols are in "async_boost" module, use require to get access to it. .. code-block:: das require daslib/async_boost + +.. seealso:: + + :ref:`tutorial_async` — Tutorial 49: Async / Await. + + :ref:`stdlib_coroutines` — coroutines module (underlying generator framework). diff --git a/examples/debugapi/heartbeat.das b/examples/debugapi/heartbeat.das deleted file mode 100644 index 70749f3c96..0000000000 --- a/examples/debugapi/heartbeat.das +++ /dev/null @@ -1,147 +0,0 @@ -// Heartbeat — Periodic Callback Injection -// -// Demonstrates `daslib/heartbeat` — a compile-time macro that -// automatically injects `heartbeat()` calls into every function -// body and every loop (for/while). This lets you run a callback -// periodically even inside long-running or infinite loops. -// -// Use cases: -// - Cooperative multitasking (yield to scheduler) -// - Watchdog / timeout detection (break infinite loops) -// - Progress reporting during heavy computation -// - Frame budget enforcement in game loops -// -// How it works: -// 1. `require daslib/heartbeat` activates a macro pass that -// adds `heartbeat()` calls to function entries and loop bodies -// 2. `set_heartbeat(callback)` registers a lambda to be called -// on each injected heartbeat point -// 3. The callback fires on every function call and every loop -// iteration — use a counter to throttle expensive work -// -// Note: lambda captures are by-copy for value types, so shared -// counters and flags must be module-level variables. -// -// Run: daslang.exe examples/debugapi/heartbeat.das - -options gen2 - -require daslib/heartbeat - -// Module-level state shared between heartbeat callbacks and code. -// Lambda captures copy value types, so locals can't be shared -// with the callback — use module-level vars instead. -var hb_count : int = 0 -var hb_should_stop : bool = false - -// ============================================================ -// Example 1: Watchdog for infinite loops -// ============================================================ -// The heartbeat fires on every loop iteration. After a -// threshold, the callback sets a flag that the loop checks. - -def demo_infinite_loop_watchdog() { - print("=== heartbeat: watchdog for infinite loops ===\n") - - hb_count = 0 - hb_should_stop = false - let max_iterations = 50 - - set_heartbeat() @ { - hb_count++ - if (hb_count >= max_iterations) { - hb_should_stop = true - } - } - - // Without the heartbeat watchdog, this runs forever. - // The heartbeat macro injects `heartbeat()` at the top of - // the loop body, so our callback fires every iteration. - var sum = 0 - while (!hb_should_stop) { - sum += 1 - } - print(" watchdog stopped loop after {hb_count} heartbeats, sum={sum}\n") -} - -// ============================================================ -// Example 2: Progress reporting during computation -// ============================================================ -// Throttle the heartbeat callback to report every N beats. - -def heavy_computation() : int { - var result = 0 - for (i in range(1000)) { - result += i * i - } - return result -} - -def demo_progress_reporting() { - print("\n=== heartbeat: progress reporting ===\n") - - hb_count = 0 - - set_heartbeat() @ { - hb_count++ - if (hb_count % 200 == 0) { - print(" ... {hb_count} heartbeats so far\n") - } - } - - let result = heavy_computation() - print(" computation result = {result}\n") - print(" total heartbeats = {hb_count}\n") -} - -// ============================================================ -// Example 3: Nested function calls -// ============================================================ -// Heartbeat fires at the start of EVERY function body, -// not just loops. - -def inner() { - // heartbeat() is injected here automatically - pass -} - -def middle() { - // heartbeat() is injected here automatically - inner() - inner() -} - -def outer() { - // heartbeat() is injected here automatically - middle() -} - -def demo_nested_calls() { - print("\n=== heartbeat: nested function calls ===\n") - - hb_count = 0 - set_heartbeat() @ { - hb_count++ - } - - outer() - // outer(1) + middle(1) + inner(1) + inner(1) = 4 - // (plus heartbeats from other function entries) - print(" heartbeats from outer->middle->inner: {hb_count}\n") -} - -// ============================================================ -// Main -// ============================================================ - -[export] -def main() { - demo_infinite_loop_watchdog() - demo_progress_reporting() - demo_nested_calls() - - // Clear the heartbeat by setting a no-op callback - set_heartbeat() @ { - pass - } -} diff --git a/examples/test/misc/apply_example.das b/examples/test/misc/apply_example.das deleted file mode 100644 index c84665ddfc..0000000000 --- a/examples/test/misc/apply_example.das +++ /dev/null @@ -1,43 +0,0 @@ -options gen2 -require daslib/apply -require daslib/json -require daslib/json_boost - -struct Bar { - x, y : float -} - -struct Foo { - i : int - s : string - q : double[3] - b : Bar -} - -def saveToJson(value : auto[]) : JsonValue? { - var arr : array - for (x in value) { - push(arr, saveToJson(x)) - } - return JV(arr) -} - -def saveToJson(value) : JsonValue? { - static_if (typeinfo is_struct(value)) { - var tab : table - apply(value) <| $(name : string; field) { - tab.insert(name, saveToJson(field)) - } - return JV(tab) - } else { - return JV(value) - } -} - -[export] -def test { - let foo = Foo(i = 1, s = "hello", q = fixed_array(1.0lf, 2.0lf, 3.0lf)) - let js_foo = saveToJson(foo) - print(write_json(js_foo)) - return true -} diff --git a/examples/test/misc/async_example.das b/examples/test/misc/async_example.das deleted file mode 100644 index efb77071e8..0000000000 --- a/examples/test/misc/async_example.das +++ /dev/null @@ -1,147 +0,0 @@ -options gen2 -require fio -require strings -require daslib/async_boost -require daslib/coroutines - - -let begin = ref_time_ticks() - -def log(msg) { - print("{fmt(":0.3f", float(get_time_usec(begin)) / 1000000.)}: {msg}") -} - -struct Data { - a : int - b : string - arr : array -} - -[async] def timeout(time : float; var callback : lambda < void >) : void { - let start_time = get_clock() - while (get_clock() - start_time < double(time)) { - await_next_frame() - } - invoke(callback) -} - - -[async] def get_plus_1(a : int) : int { - log("get_plus_1 1\n") - await_next_frame() // frame pause - log("get_plus_1 2\n") - yield a + 1 -} - // return false // force exit - -[async] def get_data(a : int) : Data { - if (a < 0) { - return false // returns empty data: [[Data]] - } - log("get_data 1\n") - await_next_frame() - log("get_data 2\n") - var data : Data - data.a = a - data.b = "hello" - data.arr <- array(1) - yield <- data -} - -[async] def async_range() : int { - yield 1 - yield 2 - yield 3 -} - -[async] def bool_range() : bool { - yield true - yield false - yield true -} - -[coroutine] def just_coroutine(n) { - for (i in 0..n) { - yield true - } -} - -[async] def getall() : void { - log("timeout...\n") - await <| timeout(1.0, @() { log("timeout done\n"); }) - var a = await <| get_plus_1(100) - debug(a, "1+100") - assert(a == 101) - - a <- await <| get_plus_1(200) - debug(a, "1+200") - assert(a == 201) - - log("pass frame\n") - await_next_frame() - - // await <| get_data(1) // 40102: call annotated by AwaitMacro failed to transform, result should be assigned to a variable, use 'let res <- await(get_data(1))' - let res <- await <| get_data(1) - debug(res, "res") - assert(res.a == 1) - assert(res.b == "hello") - assert(res.arr[0] == 1) - // res = await <| get_data(1) // 30507: this type can't be copied, Data& -const = Data - - let noRes <- await <| get_data(-1) // empty result - debug(noRes, "noRes") - assert(noRes.a == 0) - assert(noRes.b == "") - assert(length(noRes.arr) == 0) -} - - -// coroutine -[async] def getrealall() : void { - log("getrealall 1\n") - await_next_frame() - log("getrealall 2\n") - await_next_frame() - log("getrealall 3\n") - await <| getall() // ok, async function with void result - - log("async_range\n") - var ints = 0 - for (idx, i in count(), async_range()) { - print("int {i}\n") - assert(i is res) - assert(idx + 1 == (i as res)) - ints++ - await_next_frame() - } - assert(ints == 3) - - log("bool_range\n") - var bools = 0 - for (idx, b in count(), bool_range()) { - print("bool {b}\n") - assert(b is res) - assert(b as res == (idx % 2 == 0)) - bools++ - await_next_frame() - } - assert(bools == 3) - - let n = 5 - log("wait coroutine ({n} frames)\n") - await <| just_coroutine(n) - log("coroutine done\n") -} - - -[export] -def main() { - log("----\n") - var all <- getrealall() - var foo : bool - while (next(all, foo)) { - log("step...\n") - sleep(500u) - } - log("----\n") -} diff --git a/src/builtin/module_builtin_ast_annotations.cpp b/src/builtin/module_builtin_ast_annotations.cpp index b53ea516d1..5a5faae516 100644 --- a/src/builtin/module_builtin_ast_annotations.cpp +++ b/src/builtin/module_builtin_ast_annotations.cpp @@ -199,6 +199,7 @@ namespace das { AstExprCallMacroAnnotation(ModuleLibrary & ml) : AstExprLooksLikeCallAnnotation ("ExprCallMacro", ml) { addField("macro"); + addField("inFunction"); } }; diff --git a/tests/async/test_async_await.das b/tests/async/test_async_await.das new file mode 100644 index 0000000000..fdd3a811b4 --- /dev/null +++ b/tests/async/test_async_await.das @@ -0,0 +1,207 @@ +// Test: async await mechanics +// Tests let/var await, copy/move/clone assign await, +// void await, nested await, and await inside loops. + +options gen2 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require dastest/testing_boost public +require daslib/async_boost + +// ---- helpers ---- + +[async] +def add_one(x : int) : int { + await_next_frame() + yield x + 1 +} + +[async] +def slow_add(x : int; y : int) : int { + await_next_frame() + await_next_frame() + yield x + y +} + +struct Data { + value : int + tag : string +} + +[async] +def make_data(v : int; l : string) : Data { + await_next_frame() + var d : Data + d.value = v + d.tag = l + yield <- d +} + +[async] +def void_delay(n : int) : void { + for (i in range(n)) { + await_next_frame() + } +} + +// ---- let await (move assign) ---- + +[async] +def test_let_await_impl() : void { + let a <- await <| add_one(10) + assert(a == 11) +} + +[test] +def test_let_await(t : T?) { + t |> run("let x <- await typed async") @(t : T?) { + var it <- test_let_await_impl() + async_run(it) + t |> success(true) + } +} + +// ---- var copy assign await ---- + +[async] +def test_copy_await_impl() : void { + var a = 0 + a = await <| add_one(99) + assert(a == 100) +} + +[test] +def test_copy_await(t : T?) { + t |> run("x = await copies result") @(t : T?) { + var it <- test_copy_await_impl() + async_run(it) + t |> success(true) + } +} + +// ---- var move assign await ---- + +[async] +def test_move_await_impl() : void { + var a = 0 + a <- await <| add_one(49) + assert(a == 50) +} + +[test] +def test_move_await(t : T?) { + t |> run("x <- await moves result") @(t : T?) { + var it <- test_move_await_impl() + async_run(it) + t |> success(true) + } +} + +// ---- struct await (move) ---- + +[async] +def test_struct_await_impl() : void { + let d <- await <| make_data(42, "hello") + assert(d.value == 42) + assert(d.tag == "hello") +} + +[test] +def test_struct_await(t : T?) { + t |> run("let d <- await struct async") @(t : T?) { + var it <- test_struct_await_impl() + async_run(it) + t |> success(true) + } +} + +// ---- void await ---- + +[async] +def test_void_await_impl() : void { + await <| void_delay(3) +} + +[test] +def test_void_await(t : T?) { + t |> run("await void async") @(t : T?) { + var it <- test_void_await_impl() + var frames = 0 + for (v in it) { + frames ++ + } + // void_delay(3) takes 3 frames, so parent should take >= 3 + t |> success(frames >= 3) + } +} + +// ---- nested await: A awaits B awaits C ---- + +[async] +def inner_task(x : int) : int { + await_next_frame() + yield x * 2 +} + +[async] +def middle_task(x : int) : int { + let v <- await <| inner_task(x) + yield v + 1 +} + +[async] +def outer_task() : void { + let v <- await <| middle_task(10) + assert(v == 21) // 10*2 + 1 +} + +[test] +def test_nested_await(t : T?) { + t |> run("three-level nested await") @(t : T?) { + var it <- outer_task() + async_run(it) + t |> success(true) + } +} + +// ---- await in loop ---- + +[async] +def sum_sequence(n : int) : void { + var total = 0 + for (i in range(1, n + 1)) { + let v <- await <| add_one(i - 1) // returns i + total += v + } + assert(total == n * (n + 1) / 2) +} + +[test] +def test_await_in_loop(t : T?) { + t |> run("await inside a for loop") @(t : T?) { + var it <- sum_sequence(5) + async_run(it) + t |> success(true) + } +} + +// ---- chained await ---- + +[async] +def chained_computation() : void { + var x = 0 + x = await <| add_one(0) // 1 + x = await <| add_one(x) // 2 + x = await <| add_one(x) // 3 + assert(x == 3) +} + +[test] +def test_chained_await(t : T?) { + t |> run("sequential await calls chain results") @(t : T?) { + var it <- chained_computation() + async_run(it) + t |> success(true) + } +} diff --git a/tests/async/test_async_basic.das b/tests/async/test_async_basic.das new file mode 100644 index 0000000000..1ddd27feb1 --- /dev/null +++ b/tests/async/test_async_basic.das @@ -0,0 +1,231 @@ +// Test: async_boost basic functionality +// Tests [async] annotation, void and typed async functions, +// await_next_frame, yield, early return, and frame counting. + +options gen2 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require dastest/testing_boost public +require daslib/async_boost + +// ---- void async ---- + +[async] +def void_task() : void { + await_next_frame() + await_next_frame() +} + +[test] +def test_void_async(t : T?) { + t |> run("void async produces iterator") @(t : T?) { + var it <- void_task() + var count = 0 + for (v in it) { + count ++ + } + // 2 await_next_frame + implicit yield on entry = at least 2 frames + t |> success(count >= 2) + } + t |> run("void async drives with next()") @(t : T?) { + var it <- void_task() + var v : bool + var count = 0 + while (next(it, v)) { + count ++ + } + t |> success(count >= 2) + } +} + +// ---- typed async (int) ---- + +[async] +def get_value(x : int) : int { + await_next_frame() + yield x * 2 +} + +[test] +def test_typed_async(t : T?) { + t |> run("typed async yields variant") @(t : T?) { + var found_value = false + var result = 0 + for (v in get_value(21)) { + if (v is res) { + result = v as res + found_value = true + } + } + t |> success(found_value) + t |> equal(result, 42) + } +} + +// ---- typed async (struct) ---- + +struct Payload { + x : int + name : string +} + +[async] +def get_payload(n : int) : Payload { + await_next_frame() + var p : Payload + p.x = n + p.name = "hello" + yield <- p +} + +[test] +def test_struct_async(t : T?) { + t |> run("struct async yields and moves result") @(t : T?) { + var found = false + var px = 0 + var pname = "" + for (v in get_payload(7)) { + if (v is res) { + px = (v as res).x + pname = (v as res).name + found = true + } + } + t |> success(found) + t |> equal(px, 7) + t |> equal(pname, "hello") + } +} + +// ---- await_next_frame frame counting ---- + +[async] +def frame_counter(n : int) : void { + for (i in range(n)) { + await_next_frame() + } +} + +[test] +def test_frame_counting(t : T?) { + t |> run("await_next_frame suspends for exactly N frames") @(t : T?) { + var it <- frame_counter(5) + var frames = 0 + for (v in it) { + frames ++ + } + t |> equal(frames, 5) + } +} + +// ---- early return ---- + +[async] +def early_return(do_return : bool) : void { + await_next_frame() + if (do_return) { + return false + } + await_next_frame() + await_next_frame() +} + +[test] +def test_early_return(t : T?) { + t |> run("return false exits early") @(t : T?) { + var it <- early_return(true) + var frames = 0 + for (v in it) { + frames ++ + } + // Should exit after 1 frame (the first await_next_frame before if) + t |> equal(frames, 1) + } + t |> run("no early return completes normally") @(t : T?) { + var it <- early_return(false) + var frames = 0 + for (v in it) { + frames ++ + } + t |> equal(frames, 3) + } +} + +// ---- empty async ---- + +[async] +def empty_async() : void { + pass +} + +[test] +def test_empty_async(t : T?) { + t |> run("empty async completes immediately") @(t : T?) { + var it <- empty_async() + var frames = 0 + for (v in it) { + frames ++ + } + t |> equal(frames, 0) + } +} + +// ---- typed async with multiple yields ---- + +[async] +def multi_yield() : int { + yield 10 + yield 20 + yield 30 +} + +[test] +def test_multi_yield(t : T?) { + t |> run("typed async yields multiple values") @(t : T?) { + var vals : array + for (v in multi_yield()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], 10) + t |> equal(vals[1], 20) + t |> equal(vals[2], 30) + } +} + +// ---- typed async returning negative result ---- + +[async] +def get_or_default(x : int) : int { + if (x < 0) { + return false + } + await_next_frame() + yield x +} + +[test] +def test_typed_early_return(t : T?) { + t |> run("typed async with return false yields nothing") @(t : T?) { + var vals : array + for (v in get_or_default(-1)) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 0) + } + t |> run("typed async with valid input yields value") @(t : T?) { + var vals : array + for (v in get_or_default(42)) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 1) + t |> equal(vals[0], 42) + } +} diff --git a/tests/async/test_async_coroutine.das b/tests/async/test_async_coroutine.das new file mode 100644 index 0000000000..7598ba7dbc --- /dev/null +++ b/tests/async/test_async_coroutine.das @@ -0,0 +1,192 @@ +// Test: async and coroutine interop +// Tests mixing [async] with [coroutine], awaiting coroutines +// from async functions, and running both with shared runners. + +options gen2 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require dastest/testing_boost public +require daslib/async_boost +require daslib/coroutines +require strings + +// ---- await coroutine from async ---- + +var g_coroutine_steps : int = 0 + +[coroutine] +def simple_coroutine(n : int) { + for (i in range(n)) { + g_coroutine_steps ++ + co_continue() + } +} + +[async] +def async_with_coroutine() : void { + await_next_frame() + await <| simple_coroutine(3) + await_next_frame() +} + +[test] +def test_await_coroutine(t : T?) { + t |> run("await coroutine from async function") @(t : T?) { + g_coroutine_steps = 0 + var it <- async_with_coroutine() + async_run(it) + t |> equal(g_coroutine_steps, 3) + } +} + +// ---- both in same file ---- + +[coroutine] +def counting_co() : int { + yield 1 + yield 2 + yield 3 + return false +} + +[async] +def counting_async() : int { + yield 10 + yield 20 + yield 30 +} + +[test] +def test_both_in_same_file(t : T?) { + t |> run("coroutine yields direct values") @(t : T?) { + var vals : array + for (v in counting_co()) { + vals |> push(v) + } + t |> equal(length(vals), 3) + t |> equal(vals[0], 1) + t |> equal(vals[1], 2) + t |> equal(vals[2], 3) + } + t |> run("async yields variant-wrapped values") @(t : T?) { + var vals : array + for (v in counting_async()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], 10) + t |> equal(vals[1], 20) + t |> equal(vals[2], 30) + } +} + +// ---- async_run on coroutine ---- + +var g_ran_steps : int = 0 + +[coroutine] +def work_coroutine() { + g_ran_steps ++ + co_continue() + g_ran_steps ++ + co_continue() + g_ran_steps ++ +} + +[test] +def test_async_run_on_coroutine(t : T?) { + t |> run("async_run drives coroutine to completion") @(t : T?) { + g_ran_steps = 0 + var it <- work_coroutine() + async_run(it) + t |> equal(g_ran_steps, 3) + } +} + +// ---- nested: async awaits async which awaits coroutine ---- + +var g_deep_log : array + +[coroutine] +def deep_coroutine() { + g_deep_log |> push("co:1") + co_continue() + g_deep_log |> push("co:2") +} + +[async] +def mid_async() : void { + g_deep_log |> push("mid:start") + await <| deep_coroutine() + g_deep_log |> push("mid:end") +} + +[async] +def top_async() : void { + g_deep_log |> push("top:start") + await <| mid_async() + g_deep_log |> push("top:end") +} + +[test] +def test_deep_nesting(t : T?) { + t |> run("async -> async -> coroutine nesting") @(t : T?) { + g_deep_log |> clear() + var it <- top_async() + async_run(it) + t |> equal(length(g_deep_log), 6) + t |> equal(g_deep_log[0], "top:start") + t |> equal(g_deep_log[1], "mid:start") + t |> equal(g_deep_log[2], "co:1") + t |> equal(g_deep_log[3], "co:2") + t |> equal(g_deep_log[4], "mid:end") + t |> equal(g_deep_log[5], "top:end") + } +} + +// ---- cr_run_all with mix of coroutines ---- + +var g_mix_log : array + +[coroutine] +def mix_co_a() { + g_mix_log |> push("a:1") + co_continue() + g_mix_log |> push("a:2") +} + +[coroutine] +def mix_co_b() { + g_mix_log |> push("b:1") + co_continue() + g_mix_log |> push("b:2") + co_continue() + g_mix_log |> push("b:3") +} + +[test] +def test_cr_run_all_coroutines(t : T?) { + t |> run("cr_run_all with multiple coroutines") @(t : T?) { + g_mix_log |> clear() + var tasks : Coroutines + tasks |> emplace <| mix_co_a() + tasks |> emplace <| mix_co_b() + cr_run_all(tasks) + // Both should complete + var a_count = 0 + var b_count = 0 + for (entry in g_mix_log) { + if (starts_with(entry, "a:")) { + a_count ++ + } + if (starts_with(entry, "b:")) { + b_count ++ + } + } + t |> equal(a_count, 2) + t |> equal(b_count, 3) + } +} diff --git a/tests/async/test_async_iter.das b/tests/async/test_async_iter.das new file mode 100644 index 0000000000..19b0c7a155 --- /dev/null +++ b/tests/async/test_async_iter.das @@ -0,0 +1,184 @@ +// Test: iterating async generators +// Tests for-loop over typed async generators, zip with count(), +// bool-typed async, and await_next_frame inside iteration. + +options gen2 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require dastest/testing_boost public +require daslib/async_boost + +// ---- basic async range ---- + +[async] +def int_range() : int { + yield 10 + yield 20 + yield 30 +} + +[test] +def test_async_int_range(t : T?) { + t |> run("iterate typed async with for loop") @(t : T?) { + var vals : array + for (v in int_range()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], 10) + t |> equal(vals[1], 20) + t |> equal(vals[2], 30) + } +} + +// ---- bool async range ---- + +[async] +def bool_range() : bool { + yield true + yield false + yield true +} + +[test] +def test_async_bool_range(t : T?) { + t |> run("iterate bool async") @(t : T?) { + var vals : array + for (v in bool_range()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], true) + t |> equal(vals[1], false) + t |> equal(vals[2], true) + } +} + +// ---- zip with count ---- + +[async] +def indexed_range() : int { + yield 100 + yield 200 + yield 300 +} + +[test] +def test_zip_with_count(t : T?) { + t |> run("async generator zipped with count()") @(t : T?) { + var indices : array + var values : array + for (idx, v in count(), indexed_range()) { + indices |> push(idx) + if (v is res) { + values |> push(v as res) + } + } + t |> equal(length(indices), 3) + // indices include wait frames too, so check values + t |> equal(length(values), 3) + t |> equal(values[0], 100) + t |> equal(values[1], 200) + t |> equal(values[2], 300) + } +} + +// ---- async range with frame pauses ---- + +[async] +def spaced_range() : int { + yield 1 + await_next_frame() + yield 2 + await_next_frame() + yield 3 +} + +[test] +def test_spaced_range(t : T?) { + t |> run("async range with await_next_frame between yields") @(t : T?) { + var vals : array + var total_frames = 0 + for (v in spaced_range()) { + total_frames ++ + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], 1) + t |> equal(vals[1], 2) + t |> equal(vals[2], 3) + // 3 values + 2 waits = 5 total iterations + t |> equal(total_frames, 5) + } +} + +// ---- empty async range ---- + +[async] +def empty_range() : int { + return false +} + +[test] +def test_empty_range(t : T?) { + t |> run("empty async range produces no values") @(t : T?) { + var count = 0 + for (v in empty_range()) { + count ++ + } + t |> equal(count, 0) + } +} + +// ---- single yield ---- + +[async] +def single_value() : int { + yield 42 +} + +[test] +def test_single_value(t : T?) { + t |> run("async with single yield") @(t : T?) { + var vals : array + for (v in single_value()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 1) + t |> equal(vals[0], 42) + } +} + +// ---- string async range ---- + +[async] +def string_range() : string { + yield "alpha" + yield "beta" + yield "gamma" +} + +[test] +def test_string_range(t : T?) { + t |> run("string-typed async range") @(t : T?) { + var vals : array + for (v in string_range()) { + if (v is res) { + vals |> push(v as res) + } + } + t |> equal(length(vals), 3) + t |> equal(vals[0], "alpha") + t |> equal(vals[1], "beta") + t |> equal(vals[2], "gamma") + } +} diff --git a/tests/async/test_async_run.das b/tests/async/test_async_run.das new file mode 100644 index 0000000000..08f118a211 --- /dev/null +++ b/tests/async/test_async_run.das @@ -0,0 +1,151 @@ +// Test: async runner functions +// Tests async_run, async_run_all, async_timeout, async_race. + +options gen2 +options no_unused_function_arguments = false +options no_unused_block_arguments = false + +require dastest/testing_boost public +require daslib/async_boost +require strings + +// ---- helpers ---- + +var g_log : array + +[async] +def logging_task(name : string; steps : int) : void { + for (i in range(steps)) { + g_log |> push("{name}:{i}") + await_next_frame() + } +} + +[async] +def quick_task() : void { + await_next_frame() +} + +[async] +def slow_task() : void { + for (i in range(10)) { + await_next_frame() + } +} + +// ---- async_run ---- + +[test] +def test_async_run(t : T?) { + t |> run("async_run drives single task to completion") @(t : T?) { + g_log |> clear() + var it <- logging_task("a", 3) + async_run(it) + t |> equal(length(g_log), 3) + t |> equal(g_log[0], "a:0") + t |> equal(g_log[1], "a:1") + t |> equal(g_log[2], "a:2") + } +} + +// ---- async_run_all ---- + +[test] +def test_async_run_all(t : T?) { + t |> run("async_run_all drives multiple tasks") @(t : T?) { + g_log |> clear() + var tasks : array> + tasks |> emplace <| logging_task("x", 2) + tasks |> emplace <| logging_task("y", 3) + async_run_all(tasks) + // Both tasks should have run to completion + var x_count = 0 + var y_count = 0 + for (entry in g_log) { + if (starts_with(entry, "x:")) { + x_count ++ + } + if (starts_with(entry, "y:")) { + y_count ++ + } + } + t |> equal(x_count, 2) + t |> equal(y_count, 3) + } +} + +// ---- async_timeout: completes ---- + +[test] +def test_async_timeout_completes(t : T?) { + t |> run("async_timeout returns true when task completes in time") @(t : T?) { + var it <- quick_task() + let completed = async_timeout(it, 100) + t |> success(completed) + } +} + +// ---- async_timeout: expires ---- + +[test] +def test_async_timeout_expires(t : T?) { + t |> run("async_timeout returns false when task exceeds limit") @(t : T?) { + var it <- slow_task() + let completed = async_timeout(it, 3) + t |> success(!completed) + } +} + +// ---- async_timeout: zero frames ---- + +[test] +def test_async_timeout_zero(t : T?) { + t |> run("async_timeout with 0 frames always times out") @(t : T?) { + var it <- quick_task() + let completed = async_timeout(it, 0) + t |> success(!completed) + } +} + +// ---- async_race: first finishes first ---- + +[test] +def test_async_race_first_wins(t : T?) { + t |> run("async_race returns 0 when first task is faster") @(t : T?) { + var a <- quick_task() + var b <- slow_task() + let winner = async_race(a, b) + t |> equal(winner, 0) + } +} + +// ---- async_race: second finishes first ---- + +[test] +def test_async_race_second_wins(t : T?) { + t |> run("async_race returns 1 when second task is faster") @(t : T?) { + var a <- slow_task() + var b <- quick_task() + let winner = async_race(a, b) + t |> equal(winner, 1) + } +} + +// ---- async_race: both same length ---- + +[async] +def two_frame_task() : void { + await_next_frame() + await_next_frame() +} + +[test] +def test_async_race_tie(t : T?) { + t |> run("async_race with equal tasks returns 0 (first checked)") @(t : T?) { + var a <- two_frame_task() + var b <- two_frame_task() + let winner = async_race(a, b) + // Both finish at same time, but 'a' is checked first + t |> equal(winner, 0) + } +} diff --git a/tutorials/language/48_apply.das b/tutorials/language/48_apply.das new file mode 100644 index 0000000000..f302fd4405 --- /dev/null +++ b/tutorials/language/48_apply.das @@ -0,0 +1,326 @@ +// Tutorial 48: Compile-Time Field Iteration with apply +// +// This tutorial covers: +// - Using apply to iterate struct fields at compile time +// - Compile-time dispatch with static_if on field names +// - Mutating fields through apply +// - Iterating tuple fields (unnamed and named) +// - Iterating variant alternatives +// - Building a generic describe function +// - Reading field annotations with the 3-argument form +// +// Prerequisites: None (basic daScript knowledge only) +// +// Key concepts: +// - apply(value) $(name, field) { ... } iterates all fields of a struct, tuple, or variant +// - name is a compile-time string constant — static_if can branch on it +// - apply generates specialized code per field — no runtime reflection overhead +// - The 3-arg form $(name, field, annotations) gives access to per-field metadata +// - apply works on structs, tuples (named and unnamed), and variants +// +// Run: daslang.exe tutorials/language/48_apply.das + +options gen2 +options rtti + +require rtti +require daslib/apply +require daslib/strings_boost + + +// ============================================================ +// Section 1: Basic struct iteration +// ============================================================ +// +// apply(value) $(name, field) { ... } visits every field of a struct. +// `name` is the field name (string constant), `field` is the value. +// The block is generated once per field at compile time, so every +// field gets its own type-correct code path. + +struct Hero { + name : string + health : int + speed : float +} + +def demo_basic_struct() { + print("\n=== Section 1: Basic struct iteration ===\n") + + let hero = Hero(name = "Archer", health = 100, speed = 3.5) + + // apply iterates every field — no reflection, pure compile-time expansion + apply(hero) $(name, field) { + print(" {name} = {field}\n") + } + // output: + // name = Archer + // health = 100 + // speed = 3.5 +} + + +// ============================================================ +// Section 2: Compile-time dispatch with static_if +// ============================================================ +// +// Because `name` is known at compile time, you can branch on it +// with static_if. Only the matching branch is compiled for each +// field — the others are discarded entirely. + +struct Config { + width : int + height : int + title : string + fullscreen : bool +} + +def demo_static_if() { + print("\n=== Section 2: Compile-time dispatch with static_if ===\n") + + let cfg = Config(width = 1920, height = 1080, title = "My Game", fullscreen = true) + + apply(cfg) $(name, field) { + static_if (name == "title") { + print(" Title (special handling): \"{field}\"\n") + } else { + print(" {name} = {field}\n") + } + } + // output: + // width = 1920 + // height = 1080 + // Title (special handling): "My Game" + // fullscreen = true +} + + +// ============================================================ +// Section 3: Mutating fields +// ============================================================ +// +// If you pass a mutable variable (var), apply gives you mutable +// references to each field, allowing in-place modification. + +struct Stats { + attack : int + defense : int + magic : int +} + +def demo_mutation() { + print("\n=== Section 3: Mutating fields ===\n") + + var stats = Stats(attack = 10, defense = 5, magic = 8) + print(" Before: attack={stats.attack}, defense={stats.defense}, magic={stats.magic}\n") + + // Double every integer field + apply(stats) $(name, field) { + field *= 2 + } + + print(" After: attack={stats.attack}, defense={stats.defense}, magic={stats.magic}\n") + // output: + // Before: attack=10, defense=5, magic=8 + // After: attack=20, defense=10, magic=16 +} + + +// ============================================================ +// Section 4: Tuples — unnamed and named +// ============================================================ +// +// apply also works on tuples. For unnamed tuples the field names +// are "_0", "_1", etc. Named tuples use their declared names. + +def demo_tuples() { + print("\n=== Section 4: Tuples ===\n") + + // Unnamed tuple + let pair : tuple = (42, "hello") + print(" Unnamed tuple:\n") + apply(pair) $(name, field) { + print(" {name} = {field}\n") + } + // output: + // Unnamed tuple: + // _0 = 42 + // _1 = hello + + // Named tuple + let point : tuple = (1.0, 2.0) + print(" Named tuple:\n") + apply(point) $(name, field) { + print(" {name} = {field}\n") + } + // output: + // Named tuple: + // x = 1 + // y = 2 +} + + +// ============================================================ +// Section 5: Variants +// ============================================================ +// +// For variants, apply visits only the currently active alternative. +// The block fires for the one alternative that is set. + +variant Shape { + circle : float // radius + rect : float2 // width, height + triangle : float3 // three side lengths +} + +def demo_variants() { + print("\n=== Section 5: Variants ===\n") + + var shapes : array + shapes |> emplace(Shape(circle = 5.0)) + shapes |> emplace(Shape(rect = float2(3.0, 4.0))) + shapes |> emplace(Shape(triangle = float3(3.0, 4.0, 5.0))) + + for (s in shapes) { + apply(s) $(name, field) { + print(" Shape is {name}: {field}\n") + } + } + // output: + // Shape is circle: 5 + // Shape is rect: 3,4 + // Shape is triangle: 3,4,5 +} + + +// ============================================================ +// Section 6: Practical example — generic describe +// ============================================================ +// +// apply is perfect for building generic utilities that work on +// any struct without knowing its fields in advance. + +struct Weapon { + name : string + damage : int + weight : float +} + +struct Potion { + name : string + effect : string + uses : int +} + +def describe(value) { + //! Print a human-readable one-line description of any struct. + var first = true + print("\{") + apply(value) $(name, field) { + if (!first) { + print(", ") + } + first = false + // Use static_if on the type to add quotes around strings + static_if (typeinfo stripped_typename(field) == "string") { + print("{name}=\"{field}\"") + } else { + print("{name}={field}") + } + } + print("\}") +} + +def demo_describe() { + print("\n=== Section 6: Generic describe ===\n") + + let sword = Weapon(name = "Excalibur", damage = 50, weight = 3.2) + let potion = Potion(name = "Heal", effect = "restore_hp", uses = 3) + let hero = Hero(name = "Knight", health = 200, speed = 2.0) + + print(" ") + describe(sword) + print("\n ") + describe(potion) + print("\n ") + describe(hero) + print("\n") + // output: + // {name="Excalibur", damage=50, weight=3.2} + // {name="Heal", effect="restore_hp", uses=3} + // {name="Knight", health=200, speed=2} +} + + +// ============================================================ +// Section 7: Field annotations (3-argument form) +// ============================================================ +// +// Struct fields can carry metadata via annotations: +// @annotation_name field : type (bool, defaults to true) +// @annotation_name=value field : type (int, float, or string) +// @annotation_name="text" field : type (string with quotes) +// +// The 3-argument form of apply receives these annotations as +// array> for each field. +// This enables custom serialization, validation, or display logic +// driven by declarative metadata. + +struct DbRecord { + @column = "user_name" name : string + @column = "user_email" email : string + @skip id : int + @column = "age" age : int +} + +def demo_annotations() { + print("\n=== Section 7: Field annotations ===\n") + + let record = DbRecord(name = "Alice", email = "alice@example.com", id = 42, age = 30) + + // Build a pseudo-SQL insert using field annotations + var columns : array + var values : array + + apply(record) $(name : string; field; annotations) { + var column_name = name + var skip = false + for (ann in annotations) { + if (ann.name == "skip") { + skip = true + } elif (ann.name == "column") { + column_name = ann.data as tString + } + } + if (!skip) { + columns |> push(column_name) + static_if (typeinfo stripped_typename(field) == "string") { + values |> push("'{field}'") + } else { + values |> push("{field}") + } + } + } + + let sep = ", " + print(" INSERT INTO users ({join(columns, sep)})\n") + print(" VALUES ({join(values, sep)})\n") + // output: + // INSERT INTO users (user_name, user_email, age) + // VALUES ('Alice', 'alice@example.com', 30) +} + + +// ============================================================ +// Main +// ============================================================ + +[export] +def main() { + demo_basic_struct() + demo_static_if() + demo_mutation() + demo_tuples() + demo_variants() + demo_describe() + demo_annotations() +} diff --git a/tutorials/language/49_async.das b/tutorials/language/49_async.das new file mode 100644 index 0000000000..695dd51765 --- /dev/null +++ b/tutorials/language/49_async.das @@ -0,0 +1,313 @@ +// Tutorial 49: Async / Await +// +// This tutorial covers: +// - The [async] annotation (builds on Tutorial 40: Coroutines) +// - Void async functions and await_next_frame() +// - Typed async functions that yield values via variant +// - await — waiting for an async result +// - Struct returns with move semantics +// - Iterating async generators +// - Running tasks: async_run, async_run_all, async_timeout, async_race +// - Mixing [async] with [coroutine] +// +// Prerequisites: Tutorial 15 (Iterators and Generators), +// Tutorial 40 (Coroutines) +// +// Run: daslang.exe tutorials/language/49_async.das + +options gen2 +options no_unused_function_arguments = false + +require daslib/async_boost +require daslib/coroutines + +// ============================================================ +// Section 1: Void async — the simplest form +// ============================================================ +// An [async] function with `: void` return type becomes a +// generator state machine — identical to a [coroutine]. +// Use await_next_frame() to suspend until the next step. + +[async] +def greet(name : string) : void { + print(" hello, ") + await_next_frame() + print("{name}!\n") +} + +def demo_void_async() { + print("=== void async ===\n") + // Create the async task — returns iterator + var it <- greet("world") + // Step through manually + var step = 1 + for (running in it) { + print(" -- step {step} --\n") + step ++ + } + print(" -- done --\n") +} + +// ============================================================ +// Section 2: Typed async — yielding values +// ============================================================ +// An [async] function with a non-void return type yields values +// wrapped in variant. Each await_next_frame() +// yields variant(wait=true); each `yield value` yields +// variant(res=value). + +[async] +def compute(x : int) : int { + await_next_frame() // simulate one frame of work + yield x * 2 +} + +def demo_typed_async() { + print("\n=== typed async ===\n") + for (v in compute(21)) { + if (v is wait) { + print(" (waiting...)\n") + } elif (v is res) { + print(" result = {v as res}\n") // 42 + } + } +} + +// ============================================================ +// Section 3: Await — waiting for an async result +// ============================================================ +// Inside an [async] function you can await another async call. +// `let result <- await <| fn(args)` suspends the parent until +// the child completes. + +[async] +def add_one(x : int) : int { + await_next_frame() + yield x + 1 +} + +[async] +def chained_math() : void { + var a = 0 + // Copy-assign await + a = await <| add_one(0) // a = 1 + // Move-assign await + a <- await <| add_one(a) // a = 2 + // Let-bind await + let b <- await <| add_one(a) // b = 3 + print(" a={a}, b={b}\n") +} + +def demo_await() { + print("\n=== await ===\n") + var it <- chained_math() + async_run(it) +} + +// ============================================================ +// Section 4: Struct return with move semantics +// ============================================================ +// Async functions can yield structs. Non-copyable data uses +// yield <- to move the result out. + +struct Measurement { + sensor_id : int + value : float + tag : string +} + +[async] +def read_sensor(id : int) : Measurement { + await_next_frame() // simulate I/O delay + var m : Measurement + m.sensor_id = id + m.value = 3.14 + m.tag = "temperature" + yield <- m +} + +[async] +def process_sensors() : void { + let m <- await <| read_sensor(1) + print(" sensor {m.sensor_id}: {m.value} ({m.tag})\n") +} + +def demo_struct_return() { + print("\n=== struct return ===\n") + var it <- process_sensors() + async_run(it) +} + +// ============================================================ +// Section 5: Iterating async generators +// ============================================================ +// A typed async function can yield multiple values, acting as +// an asynchronous generator. Consumers iterate and check +// `v is res` to extract values. + +[async] +def fibonacci_async(n : int) : int { + var a = 0 + var b = 1 + for (i in range(n)) { + yield a + let next_val = a + b + a = b + b = next_val + await_next_frame() // simulate work each step + } +} + +def demo_async_iteration() { + print("\n=== async iteration ===\n") + print(" fibonacci: ") + for (v in fibonacci_async(8)) { + if (v is res) { + print("{v as res} ") + } + } + print("\n") +} + +// ============================================================ +// Section 6: Running tasks — async_run, async_run_all, +// async_timeout, async_race +// ============================================================ + +[async] +def worker(name : string; frames : int) : void { + for (i in range(frames)) { + print(" {name}: frame {i + 1}/{frames}\n") + await_next_frame() + } +} + +def demo_runners() { + print("\n=== async_run ===\n") + var single <- worker("solo", 3) + async_run(single) + + print("\n=== async_run_all (cooperative) ===\n") + var tasks : array> + tasks |> emplace <| worker("alpha", 2) + tasks |> emplace <| worker("beta", 3) + async_run_all(tasks) + print(" all done\n") + + print("\n=== async_timeout ===\n") + var fast <- worker("fast", 2) + let completed = async_timeout(fast, 10) + print(" fast completed: {completed}\n") // true + + var slow <- worker("slow", 100) + let timed_out = async_timeout(slow, 3) + print(" slow completed: {timed_out}\n") // false (timeout) + + print("\n=== async_race ===\n") + var racer_a <- worker("A", 2) + var racer_b <- worker("B", 5) + let winner = async_race(racer_a, racer_b) + print(" winner: {winner}\n") // 0 (A finishes first) +} + +// ============================================================ +// Section 7: Mixing async with coroutines +// ============================================================ +// An [async] function can await a [coroutine]. This is useful +// for composing low-level coroutine logic with high-level +// async orchestration. + +[coroutine] +def tick_counter(n : int) { + for (i in range(n)) { + print(" tick {i + 1}\n") + co_continue() + } +} + +[async] +def orchestrator() : void { + print(" orchestrator: start\n") + await_next_frame() + print(" orchestrator: awaiting coroutine\n") + await <| tick_counter(3) + print(" orchestrator: coroutine done\n") + await_next_frame() + print(" orchestrator: finish\n") +} + +def demo_mixed() { + print("\n=== mixing async + coroutine ===\n") + var it <- orchestrator() + async_run(it) +} + +[export] +def main() { + demo_void_async() + demo_typed_async() + demo_await() + demo_struct_return() + demo_async_iteration() + demo_runners() + demo_mixed() + print("done\n") +} + +// expected output: +// === void async === +// hello, -- step 1 -- +// world! +// -- done -- +// +// === typed async === +// (waiting...) +// result = 42 +// +// === await === +// a=2, b=3 +// +// === struct return === +// sensor 1: 3.14 (temperature) +// +// === async iteration === +// fibonacci: 0 1 1 2 3 5 8 13 +// +// === async_run === +// solo: frame 1/3 +// solo: frame 2/3 +// solo: frame 3/3 +// +// === async_run_all (cooperative) === +// beta: frame 1/3 +// alpha: frame 1/2 +// beta: frame 2/3 +// alpha: frame 2/2 +// beta: frame 3/3 +// all done +// +// === async_timeout === +// fast: frame 1/2 +// fast: frame 2/2 +// fast completed: true +// slow: frame 1/100 +// slow: frame 2/100 +// slow: frame 3/100 +// slow completed: false +// +// === async_race === +// A: frame 1/2 +// B: frame 1/5 +// A: frame 2/2 +// B: frame 2/5 +// winner: 0 +// +// === mixing async + coroutine === +// orchestrator: start +// orchestrator: awaiting coroutine +// tick 1 +// tick 2 +// tick 3 +// orchestrator: coroutine done +// orchestrator: finish +// done