diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f7853b154c..08f58089fc 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -55,7 +55,7 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - **Tuple `=>` operator:** `a => b` creates a `tuple` — useful in LINQ, table construction, and ad-hoc pairs - **Bitfield variables** need explicit type for `.field` access and printing: `var f : MyBitfield` - **Bitfield dot access:** read with `f.flag` (returns bool), write with `f.flag = true/false` -- **`typeinfo`** special syntax: `typeinfo enum_length(type)` — NOT `typeinfo(enum_length type)` +- **`typeinfo`** gen2 syntax: trait name goes **outside** parentheses — `typeinfo trait_name(type)`, NOT `typeinfo(trait_name type)`. With subtrait: `typeinfo has_method(type)`. With two traits: `typeinfo trait(type)` - **`static_if`:** `static_if (condition) { ... }` — parentheses required in gen2 ### Important defaults diff --git a/CLAUDE.md b/CLAUDE.md index f7853b154c..08f58089fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ All code examples and documentation MUST use gen2 syntax (add `options gen2` at - **Tuple `=>` operator:** `a => b` creates a `tuple` — useful in LINQ, table construction, and ad-hoc pairs - **Bitfield variables** need explicit type for `.field` access and printing: `var f : MyBitfield` - **Bitfield dot access:** read with `f.flag` (returns bool), write with `f.flag = true/false` -- **`typeinfo`** special syntax: `typeinfo enum_length(type)` — NOT `typeinfo(enum_length type)` +- **`typeinfo`** gen2 syntax: trait name goes **outside** parentheses — `typeinfo trait_name(type)`, NOT `typeinfo(trait_name type)`. With subtrait: `typeinfo has_method(type)`. With two traits: `typeinfo trait(type)` - **`static_if`:** `static_if (condition) { ... }` — parentheses required in gen2 ### Important defaults diff --git a/daslib/jobque_boost.das b/daslib/jobque_boost.das index 210aa0057a..578ceb9fb2 100644 --- a/daslib/jobque_boost.das +++ b/daslib/jobque_boost.das @@ -597,7 +597,7 @@ class private ChannelAndStatusCapture : AstCaptureMacro { ) return <- pCall } - //! This macro implements capturing of the `jobque::Channel` and `jobque::JobStatus` types. + //! This macro implements capturing of the `jobque::Channel` and `jobque::JobStatus` types. //! When captured reference counts are increased. When lambda is destroyed, reference counts are decreased. def override captureExpression(prog : Program?; mod : Module?; expr : ExpressionPtr; typ : TypeDeclPtr) : ExpressionPtr { //! Implementation details for the capture macro. diff --git a/doc/source/reference/language/macros.rst b/doc/source/reference/language/macros.rst index 9f5d3447ff..659d9c3fca 100644 --- a/doc/source/reference/language/macros.rst +++ b/doc/source/reference/language/macros.rst @@ -253,6 +253,31 @@ There is additionally the ``[enumeration_macro]`` annotation which accomplishes ``apply`` is invoked before the infer pass. It is the best time to modify the enumeration, generate some code, etc. +In gen2 syntax, register an enumeration macro and annotate enums with: + +.. code-block:: das + + [enumeration_macro(name="enum_total")] + class EnumTotalAnnotation : AstEnumerationAnnotation { + def override apply(var enu : EnumerationPtr; var group : ModuleGroup; + args : AnnotationArgumentList; + var errors : das_string) : bool { + // modify enu.list or generate code + return true + } + } + + [enum_total] + enum Direction { North; South; East; West } + +.. seealso:: + + Tutorial: :ref:`tutorial_macro_enumeration_macro` — step-by-step + enumeration macro examples (enum modification and code generation) + + Standard library: ``daslib/enum_trait.das`` — + :ref:`enum_trait module reference ` + --------------- AstVariantMacro --------------- @@ -312,13 +337,15 @@ AstReaderMacro ``add_new_reader_macro`` adds a reader macro to a module. There is additionally the ``[reader_macro]`` annotation, which essentially automates the same thing. -Reader macros accept characters, collect them if necessary, and return an ``ast::Expression``: +Reader macros accept characters, collect them if necessary, and produce output +via one of two patterns: .. code-block:: das class AstReaderMacro { def abstract accept ( prog:ProgramPtr; mod:Module?; expr:ExprReader?; ch:int; info:LineInfo ) : bool def abstract visit ( prog:ProgramPtr; mod:Module?; expr:smart_ptr ) : ExpressionPtr + def abstract suffix ( prog:ProgramPtr; mod:Module?; expr:ExprReader?; info:LineInfo; var outLine:int&; var outFile:FileInfo?& ) : string } Reader macros are invoked via the ``% READER_MACRO_NAME ~ character_sequence`` syntax. @@ -364,6 +391,20 @@ In ``visit``, the collected sequence is converted into a make array ``[ch1,ch2,. More complex examples include the JsonReader macro in :ref:`daslib/json_boost ` or RegexReader in :ref:`daslib/regex_boost `. +``suffix`` is an alternative to ``visit`` — it is called immediately after ``accept`` during parsing, +before the AST is built. Instead of returning an AST node, it returns a **string** of daScript source +code that the parser re-parses. This is useful for generating top-level declarations (functions, +structs) from custom syntax. When used at module level the ``ExprReader`` node is discarded, +and the suffix text is the only output. ``SpoofInstanceReader`` in ``daslib/spoof.das`` is an example. + +The ``outLine`` and ``outFile`` parameters allow remapping line information for error reporting in the +injected code. + +.. seealso:: + + :ref:`Tutorial: Reader Macros ` — step-by-step example + of both visit and suffix patterns. + ------------ AstCallMacro ------------ @@ -405,22 +446,32 @@ Note how the name is provided in the ``[call_macro]`` annotation. AstPassMacro ------------ -``AstPassMacro`` is one macro to rule them all. It gets entire module as an input, -and can be invoked at numerous passes: +``AstPassMacro`` is one macro to rule them all. It gets the entire program as +input and can be invoked at numerous passes: .. code-block:: das class AstPassMacro { - def abstract apply ( prog:ProgramPtr; mod:Module? ) : bool + def abstract apply(prog : ProgramPtr; mod : Module?) : bool } +Five annotations control when a pass macro runs: + +- ``[infer_macro]`` — after clean type inference. Returning ``true`` re-infers. +- ``[dirty_infer_macro]`` — during each dirty inference pass. +- ``[lint_macro]`` — after successful compilation (lint phase, read-only). +- ``[global_lint_macro]`` — same as ``[lint_macro]`` but for all modules. +- ``[optimization_macro]`` — during the optimisation loop. + ``make_pass_macro`` registers a class as a pass macro. -``add_new_infer_macro`` adds a pass macro to the infer pass. The ``[infer]`` annotation accomplishes the same thing. +Typically, such macros create an ``AstVisitor`` which performs the necessary +transformations via ``visit(prog, adapter)``. -``add_new_dirty_infer_macro`` adds a pass macro to the ``dirty`` section of infer pass. The ``[dirty_infer]`` annotation accomplishes the same thing. +.. seealso:: -Typically, such macros create an ``AstVisitor`` which performs the necessary transformations. + :ref:`tutorial_macro_pass_macro` — step-by-step tutorial with lint and + infer macro examples. ---------------- AstTypeInfoMacro @@ -435,15 +486,27 @@ AstTypeInfoMacro def abstract getAstType ( var lib:ModuleLibrary; expr:smart_ptr; var errors:das_string ) : TypeDeclPtr } -``add_new_typeinfo_macro`` adds a reader macro to a module. +``add_new_typeinfo_macro`` adds a typeinfo macro to a module. There is additionally the ``[typeinfo_macro]`` annotation, which essentially automates the same thing. -``getAstChange`` returns a newly generated ast for the typeinfo expression. +The ``typeinfo`` expression uses gen2 syntax with the trait name **outside** the +parentheses:: + + typeinfo trait_name(type) // basic + typeinfo trait_name(type) // with subtrait + typeinfo trait_name(type) // with subtrait and extratrait + +``getAstChange`` returns a newly generated AST node for the typeinfo expression. Alternatively, it returns null if no changes are required, or if there is an error. In case of error, the errors string must be filled. ``getAstType`` returns the type of the new typeinfo expression. +.. seealso:: + + Tutorial: :ref:`tutorial_macro_typeinfo_macro` — step-by-step guide with three + ``getAstChange`` examples (struct description, enum names, method check). + --------------- AstForLoopMacro --------------- @@ -472,14 +535,29 @@ AstCaptureMacro class AstCaptureMacro { def abstract captureExpression ( prog:Program?; mod:Module?; expr:ExpressionPtr; etype:TypeDeclPtr ) : ExpressionPtr def abstract captureFunction ( prog:Program?; mod:Module?; var lcs:Structure?; var fun:FunctionPtr ) : void + def abstract releaseFunction ( prog:Program?; mod:Module?; var lcs:Structure?; var fun:FunctionPtr ) : void } ``add_new_capture_macro`` adds a reader macro to a module. There is additionally the ``[capture_macro]`` annotation, which essentially automates the same thing. -``captureExpression`` is called when an expression is captured. It returns a new expression, or null if no changes are required. +``captureExpression`` is called per captured variable when the lambda struct is being built. +It returns a replacement expression to wrap the capture, or null if no changes are required. + +``captureFunction`` is called once after the lambda function is generated. +Use this to inspect captured fields (``lcs``) and append code to ``(fun.body as ExprBlock).finalList`` — +which runs **after each invocation** (per-call finally), not on destruction. + +``releaseFunction`` is called once when the lambda **finalizer** is generated. +``fun`` is the finalizer function (not the lambda call function). +Code appended to ``(fun.body as ExprBlock).list`` runs on **destruction** — +after the user-written ``finally {}`` block but before the compiler-generated +field cleanup (``delete *__this``). + +.. seealso:: -``captureFunction`` is called when a function is captured. This is where custom finalization can be added to the ``final`` section of the function body. + :ref:`Tutorial: Capture Macros ` — step-by-step example + using all three hooks with an ``[audited]`` tag annotation. ---------------- AstCommentReader diff --git a/doc/source/reference/tutorials.rst b/doc/source/reference/tutorials.rst index 445b7ccebe..1ff3fde2aa 100644 --- a/doc/source/reference/tutorials.rst +++ b/doc/source/reference/tutorials.rst @@ -153,4 +153,9 @@ Run any tutorial from the project root:: tutorials/macros/06_structure_macro.rst tutorials/macros/07_block_macro.rst tutorials/macros/08_variant_macro.rst - tutorials/macros/09_for_loop_macro.rst \ No newline at end of file + tutorials/macros/09_for_loop_macro.rst + tutorials/macros/10_capture_macro.rst + tutorials/macros/11_reader_macro.rst + tutorials/macros/12_typeinfo_macro.rst + tutorials/macros/13_enumeration_macro.rst + tutorials/macros/14_pass_macro.rst \ No newline at end of file diff --git a/doc/source/reference/tutorials/14_lambdas.rst b/doc/source/reference/tutorials/14_lambdas.rst index 52e0fda3c6..83c9725a11 100644 --- a/doc/source/reference/tutorials/14_lambdas.rst +++ b/doc/source/reference/tutorials/14_lambdas.rst @@ -103,6 +103,44 @@ A function can create and return a lambda that captures state:: Each call creates an independent counter. +Lambda lifecycle and finally blocks +===================================== + +A lambda is a heap-allocated struct. The full lifecycle is: + +1. **Capture** — outer variables are copied/moved/cloned into the struct +2. **Invoke** — the lambda body runs (may be called many times) +3. **Destroy** — when the lambda is deleted or garbage collected: + a) The ``finally{}`` block runs (user cleanup code) + b) Captured fields are finalized (compiler-generated ``delete``) + c) The struct memory is freed + +Captured fields are automatically deleted on destruction unless: + +- The field was captured **by reference** (not owned) +- The field was captured **by move/clone** with ``doNotDelete`` +- The field type is POD (int, float — no cleanup needed) + +Finally block +~~~~~~~~~~~~~ + +A lambda's ``finally{}`` block runs **once** when the lambda is +**destroyed** — not after each invocation. This is different from +a *block* ``finally{}``, which runs after every call:: + + var demo <- @() { + print("body\n") + } finally { + print("destroyed\n") // runs once, on deletion + } + demo() // prints "body" + demo() // prints "body" + unsafe { delete demo; } // prints "destroyed" + +Use lambda ``finally{}`` for one-time destruction cleanup (releasing +resources, closing handles). For per-call cleanup, use scoped +variables or ``defer`` inside the lambda body. + Lambda vs block vs function pointer ===================================== diff --git a/doc/source/reference/tutorials/macros/09_for_loop_macro.rst b/doc/source/reference/tutorials/macros/09_for_loop_macro.rst index 3ff50bc0e6..fe1a3b90a0 100644 --- a/doc/source/reference/tutorials/macros/09_for_loop_macro.rst +++ b/doc/source/reference/tutorials/macros/09_for_loop_macro.rst @@ -232,6 +232,8 @@ complex transformation but the same ``AstForLoopMacro`` pattern. Previous tutorial: :ref:`tutorial_macro_variant_macro` + Next tutorial: :ref:`tutorial_macro_capture_macro` + Standard library: ``daslib/soa.das`` (``SoaForLoop`` macro) Language reference: :ref:`Macros ` — full macro system documentation diff --git a/doc/source/reference/tutorials/macros/10_capture_macro.rst b/doc/source/reference/tutorials/macros/10_capture_macro.rst new file mode 100644 index 0000000000..5e6e50c9a6 --- /dev/null +++ b/doc/source/reference/tutorials/macros/10_capture_macro.rst @@ -0,0 +1,320 @@ +.. _tutorial_macro_capture_macro: + +.. index:: + single: Tutorial; Macros; Capture Macro + single: Tutorial; Macros; capture_macro + single: Tutorial; Macros; AstCaptureMacro + single: Tutorial; Macros; lambda capture auditing + +==================================================== + Macro Tutorial 10: Capture Macros +==================================================== + +Previous tutorials transformed calls, functions, structures, blocks, +variants, and for-loops. Capture macros intercept **lambda capture** — +they fire when a lambda (or generator) captures outer variables, +letting you wrap capture expressions, inject per-invocation cleanup, +and add destruction-time release logic. + +``[capture_macro(name="X")]`` registers a class that extends +``AstCaptureMacro``. The compiler calls three methods during lambda +code generation: + +``captureExpression(prog, mod, expr, etype)`` + Called **per captured variable** when the lambda struct is being + built. ``expr`` is the expression being assigned to the capture + field (typically an ``ExprVar``). ``etype`` is the variable's type. + Return a replacement expression to wrap the capture, or + ``default`` to leave it unchanged. + +``captureFunction(prog, mod, lcs, fun)`` + Called **once** after the lambda function is generated. ``lcs`` is + the hidden lambda struct (with fields for each capture). ``fun`` + is the lambda function. Use this to inspect captured fields and + append code to ``(fun.body as ExprBlock).finalList`` — which runs + **after each invocation** (per-call finally), not on destruction. + +``releaseFunction(prog, mod, lcs, fun)`` + Called **once** when the lambda **finalizer** is generated. ``fun`` + is the finalizer function (not the lambda call function). Code + appended to ``(fun.body as ExprBlock).list`` runs on **destruction** + — after the user-written ``finally {}`` block but before the + compiler-generated field cleanup (``delete *__this``). + +.. note:: + + Code added to ``(fun.body as ExprBlock).finalList`` by + ``captureFunction`` runs after **every** lambda invocation. Code + added to ``(fun.body as ExprBlock).list`` by ``releaseFunction`` + runs **once** on destruction. The user-written ``finally {}`` on + the lambda literal also runs on destruction (in the same finalizer), + *before* ``releaseFunction`` code. + + Generators are a special case — their function body's ``finalList`` + runs on every yield iteration, which is why the standard library's + ``ChannelAndStatusCapture`` skips generators entirely. + + +Motivation +========== + +When lambdas capture complex resources (file handles, GPU objects, +reference-counted channels), it is useful to **audit** captures +automatically — log when a resource is captured and verify it after +each call — without modifying every lambda by hand. + +This tutorial builds a capture macro driven by a **tag annotation**: +only structs marked ``[audited]`` are monitored. Non-annotated types +are silently ignored. This pattern (annotation + macro) is the same +used by ``daslib/jobque_boost.das`` for ``Channel`` and ``JobStatus`` +reference counting. + +.. note:: + + Macros cannot be used in the module that defines them. This + tutorial has **two** source files: a *module* file containing the + macro definition and a *usage* file that requires the module. + + +The module file +=============== + +``capture_macro_mod.das`` defines four pieces: + +1. ``[audited]`` — a no-op structure annotation used as a tag +2. Runtime helpers — ``audit_on_capture``, ``audit_after_invoke``, and + ``audit_on_finalize`` +3. ``CaptureAuditMacro`` — the capture macro class (three hooks) + +The tag annotation +~~~~~~~~~~~~~~~~~~ + +.. code-block:: das + + [structure_macro(name=audited)] + class AuditedAnnotation : AstStructureAnnotation { + def override apply(var st : StructurePtr; var group : ModuleGroup; + args : AnnotationArgumentList; var errors : das_string) : bool { + return true // no-op tag + } + } + +This registers ``[audited]`` as a valid struct annotation. It does +nothing at compile time — the capture macro checks for it at capture +time. + +Type checking helper +~~~~~~~~~~~~~~~~~~~~ + +A ``[macro_function]`` inspects whether a ``TypeDeclPtr`` refers to a +struct with the ``[audited]`` annotation: + +.. code-block:: das + + [macro_function] + def private is_audited(typ : TypeDeclPtr) : bool { + if (!typ.isStructure || typ.structType == null) { + return false + } + for (ann in typ.structType.annotations) { + if (ann.annotation.name == "audited") { + return true + } + } + return false + } + +This iterates ``typ.structType.annotations`` — the same pattern used +by ``daslib/match.das`` to check for ``[match_as_is]``. + +captureExpression +~~~~~~~~~~~~~~~~~ + +When an ``[audited]`` variable is captured, the macro wraps the +capture expression in a call to ``audit_on_capture(value, "name")``: + +.. code-block:: das + + def override captureExpression(prog : Program?; mod : Module?; + expr : ExpressionPtr; etype : TypeDeclPtr) : ExpressionPtr { + if (!is_audited(etype)) { + return <- default + } + var field_name = "unknown" + if (expr is ExprVar) { + field_name = string((expr as ExprVar).name) + } + var inscope pCall <- new ExprCall(at = expr.at, + name := "capture_macro_mod::audit_on_capture") + pCall.arguments |> emplace_new <| clone_expression(expr) + pCall.arguments |> emplace_new <| new ExprConstString( + at = expr.at, value := field_name) + return <- pCall + } + +``audit_on_capture`` prints ``[audit] captured 'name'`` and returns +the value unchanged, so the capture proceeds normally. + +captureFunction +~~~~~~~~~~~~~~~ + +For each ``[audited]`` field in the lambda struct, the macro appends +a print call to the function body's ``finalList``: + +.. code-block:: das + + def override captureFunction(prog : Program?; mod : Module?; + var lcs : Structure?; var fun : FunctionPtr) : void { + if (fun.flags._generator) { + return // generators run finally on every yield — skip + } + for (fld in lcs.fields) { + if (!is_audited(fld._type)) { + continue + } + if (true) { // scope needed for var inscope inside a loop + var inscope pCall <- new ExprCall(at = fld.at, + name := "capture_macro_mod::audit_after_invoke") + pCall.arguments |> emplace_new <| new ExprConstString( + at = fld.at, value := string(fld.name)) + (fun.body as ExprBlock).finalList |> emplace(pCall) + } + } + } + +The ``if (true)`` wrapper is required because ``var inscope`` is not +allowed directly inside a ``for`` loop — the extra scope satisfies +the compiler. + +releaseFunction +~~~~~~~~~~~~~~~ + +For each ``[audited]`` field in the lambda struct, the macro appends +a print call to the finalizer function's body — code that runs once +on **destruction**, after the user-written ``finally {}`` block but +before the compiler-generated ``delete *__this``: + +.. code-block:: das + + def override releaseFunction(prog : Program?; mod : Module?; + var lcs : Structure?; var fun : FunctionPtr) : void { + for (fld in lcs.fields) { + if (!is_audited(fld._type)) { + continue + } + if (true) { // scope needed for var inscope inside a loop + var inscope pCall <- new ExprCall(at = fld.at, + name := "capture_macro_mod::audit_on_finalize") + pCall.arguments |> emplace_new <| new ExprConstString( + at = fld.at, value := string(fld.name)) + (fun.body as ExprBlock).list |> emplace(pCall) + } + } + } + +Note that ``releaseFunction`` appends to ``(fun.body as ExprBlock).list`` +(the finalizer body), **not** to ``finalList``. The ``fun`` parameter +here is the finalizer function — not the lambda call function received +by ``captureFunction``. + +The finalizer execution order is: + +1. User-written ``finally {}`` block (from the lambda literal) +2. ``releaseFunction`` code (this hook) +3. ``delete *__this`` — compiler-generated field destructors +4. ``delete __this`` — heap deallocation + + +The usage file +============== + +``10_capture_macro.das`` defines an ``[audited]`` struct and a plain +struct, then creates lambdas that capture them: + +.. code-block:: das + + require capture_macro_mod + + [audited] + struct Resource { + name : string + id : int + } + + struct Plain { + x : int + } + +**Section 1** — one ``[audited]`` and one plain capture: + +.. code-block:: das + + var res = Resource(name = "texture.png", id = 1) + var pl = Plain(x = 42) + var fn <- @() { + print(" body: res.name={res.name}, pl.x={pl.x}\n") + } + fn() + unsafe { delete fn; } + +Output:: + + [audit] captured 'res' + body: res.name=texture.png, pl.x=42 + [audit] after-call: 'res' still captured + about to delete fn... + [audit] releasing 'res' + +- ``[audit] captured 'res'`` — from ``captureExpression`` at lambda creation +- ``[audit] after-call`` — from ``captureFunction``'s ``finalList`` after the call +- ``[audit] releasing 'res'`` — from ``releaseFunction`` during destruction +- No messages for ``pl`` (``Plain`` has no ``[audited]`` annotation) + +**Section 2** — two ``[audited]`` captures, two calls: + +.. code-block:: das + + var a = Resource(name = "mesh.obj", id = 2) + var b = Resource(name = "shader.hlsl", id = 3) + var fn <- @() { + print(" body: a.id={a.id}, b.id={b.id}\n") + } + fn() + fn() + +Each call produces after-call messages for both ``a`` and ``b``. +On destruction, releasing messages appear once for each field. + +**Section 3** — only non-annotated types (``int``): completely silent. + + +Real-world usage +================ + +The standard library's ``ChannelAndStatusCapture`` in +``daslib/jobque_boost.das`` uses the same hook pattern: + +- ``captureExpression``: calls ``add_ref`` on captured ``Channel`` or + ``JobStatus`` pointers (increases reference count) +- ``captureFunction``: appends a ``panic`` call that fires if the + object was not properly released after each lambda invocation +- ``releaseFunction``: could be used to call ``release`` on the + captured object during destruction (complementing ``add_ref``) + +This ensures that thread-communication objects are never leaked, +without requiring any changes to user lambda code. + + +.. seealso:: + + Full source: + :download:`10_capture_macro.das <../../../../../tutorials/macros/10_capture_macro.das>`, + :download:`capture_macro_mod.das <../../../../../tutorials/macros/capture_macro_mod.das>` + + Previous tutorial: :ref:`tutorial_macro_for_loop_macro` + + Next tutorial: :ref:`tutorial_macro_reader_macro` + + Standard library: ``daslib/jobque_boost.das`` (``ChannelAndStatusCapture``) + + Language reference: :ref:`Macros ` — full macro system documentation diff --git a/doc/source/reference/tutorials/macros/11_reader_macro.rst b/doc/source/reference/tutorials/macros/11_reader_macro.rst new file mode 100644 index 0000000000..685fb301e2 --- /dev/null +++ b/doc/source/reference/tutorials/macros/11_reader_macro.rst @@ -0,0 +1,314 @@ +.. _tutorial_macro_reader_macro: + +.. index:: + single: Tutorial; Macros; Reader Macro + single: Tutorial; Macros; reader_macro + single: Tutorial; Macros; AstReaderMacro + single: Tutorial; Macros; visit vs suffix patterns + +==================================================== + Macro Tutorial 11: Reader Macros +==================================================== + +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. + +``[reader_macro(name="X")]`` registers a class that extends +``AstReaderMacro``. Reader macros are invoked with the syntax +``%X~ character_sequence %%``. The compiler calls three methods: + +``accept(prog, mod, expr, ch, info) → bool`` + Called **per character** during parsing. ``ch`` is the current + character; ``expr.sequence`` accumulates the text. Return ``true`` + to keep reading, ``false`` to stop (typically when ``%%`` is found). + +``visit(prog, mod, expr) → ExpressionPtr`` + Called during **type inference** on the ``ExprReader`` node. Must + return an AST expression that replaces the reader expression. This + is the **visit pattern** — used when the macro appears as an + expression and produces an AST value. + +``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 + parser's input stream. This is the **suffix pattern** — used when + the macro appears at module level and generates top-level daScript + declarations (functions, structs, etc.). The ``ExprReader`` node is + discarded. + +.. note:: + + The two patterns serve different contexts: + + - **Visit pattern**: the reader macro appears in an **expression** + position (e.g., ``var x = %csv~ ... %%``). ``visit()`` builds + the resulting AST node. ``suffix()`` is not used. + + - **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()`` + is never called because the parser discards the ``ExprReader`` node. + + All reader macros share the same ``accept()`` idiom for collecting + characters. The choice between visit and suffix determines where and + how the macro produces output. + + +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. + +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 + +.. note:: + + Macros cannot be used in the module that defines them. This + tutorial has **two** source files: a *module* file containing the + macro definitions and a *usage* file that requires the module. + + +The module file +=============== + +``reader_macro_mod.das`` defines two reader macros. + +The ``accept()`` idiom +~~~~~~~~~~~~~~~~~~~~~~ + +Both macros share the same standard ``accept()`` implementation — the +most common pattern in the standard library: + +.. code-block:: das + + def override accept(prog : ProgramPtr; mod : Module?; + var expr : ExprReader?; ch : int; info : LineInfo) : bool { + if (ch != '\r') { // skip carriage returns + append(expr.sequence, ch) // accumulate in expr.sequence + } + if (ends_with(expr.sequence, "%%")) { + let len = length(expr.sequence) + resize(expr.sequence, len - 2) // strip the %% + return false // stop reading + } else { + return true // keep reading + } + } + +Characters are appended to ``expr.sequence`` one at a time. When the +``%%`` terminator is detected, it is stripped and ``accept()`` returns +``false`` to signal the end of the character sequence. + +CsvReader — visit pattern +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CsvReader`` is registered with ``[reader_macro(name=csv)]``. Its +``visit()`` method splits the collected sequence by commas, trims each +value, and uses ``convert_to_expression()`` from ``daslib/ast_boost`` +to embed the resulting string array in the AST: + +.. code-block:: das + + def override visit(prog : ProgramPtr; mod : Module?; + expr : smart_ptr) : ExpressionPtr { + if (is_in_completion()) { + return <- default + } + let seq = string(expr.sequence) + var items <- split(seq, ",") + for (i in range(length(items))) { + items[i] = strip(items[i]) + } + return <- convert_to_expression(items, expr.at) + } + +``convert_to_expression`` takes any daScript 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 +regex objects. + +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: + +.. code-block:: das + + def override suffix(prog : ProgramPtr; mod : Module?; + var expr : ExprReader?; info : LineInfo; + var outLine : int&; var outFile : FileInfo?&) : string { + let seq = string(expr.sequence) + var lines <- split(seq, "\n") + var func_name = "basic_program" + var stmts : array + for (line in lines) { + let trimmed = strip(line) + if (empty(trimmed)) { continue } + if (starts_with(trimmed, "DEF ")) { + func_name = strip(slice(trimmed, 4)) + continue + } + // parse: NUMBER COMMAND args + let sp1 = find(trimmed, " ") + if (sp1 < 0) { continue } + let after_num = strip(slice(trimmed, sp1 + 1)) + if (starts_with(after_num, "PRINT ")) { + let arg = strip(slice(after_num, 6)) + if (starts_with(arg, "\"")) { + let inner = slice(arg, 1, length(arg) - 1) + stmts |> push("print(\"{inner}\\n\")") + } else { + stmts |> push("print(\"\{{arg}\}\\n\")") + } + } elif (starts_with(after_num, "LET ")) { + stmts |> push("var {strip(slice(after_num, 4))}") + } + } + var result = "def {func_name}() \{\n" + for (stmt in stmts) { + result += " {stmt}\n" + } + result += "\}\n" + return result + } + +The returned string is valid gen2 daScript code. The parser receives +this text and parses it as a normal function definition at module level. + +.. note:: + + The suffix must produce **gen2 syntax** (with braces) if the + source file uses ``options gen2``. String escaping requires care: + ``\{`` and ``\}`` produce literal braces (avoiding string + interpolation), while ``{func_name}`` interpolates the variable. + + +The usage file +============== + +``11_reader_macro.das`` demonstrates both patterns. + +**Section 1** — CSV reader (visit pattern): + +.. code-block:: das + + var data <- %csv~ Alice, 30, New York %% + print(" items ({length(data)}):\n") + for (item in data) { + print(" '{item}'\n") + } + +Output:: + + --- Section 1: CSV reader (visit pattern) --- + items (3): + 'Alice' + '30' + 'New York' + colors (3): + 'red' + 'green' + 'blue' + +The ``%csv~`` expression evaluates to an ``array`` — the CSV +values are parsed and embedded at compile time, not at runtime. + +**Section 2** — BASIC transpiler (suffix pattern): + +.. code-block:: das + + %basic~ + DEF basic_hello + 10 PRINT "Hello from BASIC" + 20 LET x = 42 + 30 PRINT x + %% + +This appears at **module level** (not inside a function). The suffix +generates a function ``basic_hello()`` that the rest of the file can +call: + +.. code-block:: das + + [export] + def main() { + basic_hello() + } + +Output:: + + --- Section 2: BASIC transpiler (suffix pattern) --- + Hello from BASIC + 42 + +The generated function is indistinguishable from hand-written code — it +participates in type checking, AOT compilation, and all other compiler +phases normally. + + +Visit vs Suffix +=============== + ++------------------+--------------------------+---------------------------+ +| Aspect | Visit pattern | Suffix pattern | ++==================+==========================+===========================+ +| Override | ``visit()`` | ``suffix()`` | ++------------------+--------------------------+---------------------------+ +| When called | Type inference | Parsing (after accept) | ++------------------+--------------------------+---------------------------+ +| Returns | AST expression | daScript source text | ++------------------+--------------------------+---------------------------+ +| Usage context | Expression position | Module level | ++------------------+--------------------------+---------------------------+ +| ExprReader fate | Replaced by visit result | Discarded by parser | ++------------------+--------------------------+---------------------------+ +| Examples (stdlib)| regex, json, stringify | spoof_instance | ++------------------+--------------------------+---------------------------+ + + +Real-world usage +================ + +The standard library includes several reader macros: + +- ``daslib/regex_boost.das`` — ``RegexReader`` (visit): compiles a + regular expression at parse time and embeds the compiled ``Regex`` + struct directly in the AST. Usage: ``%regex~pattern%%`` +- ``daslib/json_boost.das`` — ``JsonReader`` (visit): parses JSON at + compile time and embeds the resulting ``JsonValue`` tree. + Usage: ``%json~ {...} %%`` +- ``daslib/stringify.das`` — ``LongStringReader`` (visit): embeds a + multi-line string literal without escaping. + Usage: ``%stringify~ text %%`` +- ``daslib/spoof.das`` — ``SpoofTemplateReader`` (visit) + + ``SpoofInstanceReader`` (suffix): a template engine that stores + templates as strings and instantiates them by generating daScript + source code at parse time. + + +.. seealso:: + + Full source: + :download:`11_reader_macro.das <../../../../../tutorials/macros/11_reader_macro.das>`, + :download:`reader_macro_mod.das <../../../../../tutorials/macros/reader_macro_mod.das>` + + Previous tutorial: :ref:`tutorial_macro_capture_macro` + + Next tutorial: :ref:`tutorial_macro_typeinfo_macro` + + Standard library: ``daslib/regex_boost.das``, ``daslib/json_boost.das``, + ``daslib/stringify.das``, ``daslib/spoof.das`` + + Language reference: :ref:`Macros ` — full macro system documentation diff --git a/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst new file mode 100644 index 0000000000..81257c45c4 --- /dev/null +++ b/doc/source/reference/tutorials/macros/12_typeinfo_macro.rst @@ -0,0 +1,341 @@ +.. _tutorial_macro_typeinfo_macro: + +.. index:: + single: Tutorial; Macros; Typeinfo Macro + single: Tutorial; Macros; typeinfo_macro + single: Tutorial; Macros; AstTypeInfoMacro + single: Tutorial; Macros; getAstChange + +==================================================== + Macro Tutorial 12: Typeinfo Macros +==================================================== + +Previous tutorials transformed calls, functions, structures, blocks, +variants, for-loops, lambda captures, and reader macros. Typeinfo macros +extend the built-in ``typeinfo`` expression with **custom compile-time +type introspection**. + +``[typeinfo_macro(name="X")]`` registers a class that extends +``AstTypeInfoMacro``. The macro is invoked with the syntax +``typeinfo X(expr)`` (gen2) or ``typeinfo X(expr)``. During +type inference the compiler calls: + +``getAstChange(expr, errors) → ExpressionPtr`` + Receives an ``ExprTypeInfo`` node and returns a replacement AST + expression — typically a constant (string, int, bool) or an array + literal. The returned expression replaces the entire ``typeinfo`` + call at compile time. Return ``null`` with an error message to + report a compilation error. + + +ExprTypeInfo fields +------------------- + +The ``expr`` parameter exposes several fields: + +=============== ==================== ======================================= +Field Type Description +=============== ==================== ======================================= +``typeexpr`` ``TypeDeclPtr`` The type argument (from ``type``) +``subexpr`` ``ExpressionPtr`` The expression argument (if used) +``subtrait`` ``string`` The ```` in ``typeinfo X`` +``extratrait`` ``string`` The second ```` parameter +``trait`` ``string`` The name of the typeinfo trait +``at`` ``LineInfo`` Source location for error reporting +=============== ==================== ======================================= + +``typeexpr`` gives access to the full type declaration. On structure +types, ``typeexpr.structType.fields`` is a list of ``FieldDeclaration`` +nodes (each with ``name``, ``_type``, ``flags``). On enum types, +``typeexpr.enumType.list`` contains ``EnumEntry`` nodes (each with +``name``). + + +Motivation +========== + +The ``typeinfo`` keyword already provides many built-in traits: +``sizeof``, ``typename``, ``is_ref``, ``has_field``, ``is_enum``, +``is_struct``, and dozens more. But sometimes you need *domain-specific* +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. + + +The module file +=============== + +The macro module defines three ``AstTypeInfoMacro`` subclasses, each +returning a different expression type (string, array, bool). + +Full source: :download:`typeinfo_macro_mod.das <../../../../../tutorials/macros/typeinfo_macro_mod.das>` + + +struct_info — returning a string +-------------------------------- + +``typeinfo struct_info(type)`` builds a description string at compile +time: + +.. code-block:: das + + [typeinfo_macro(name="struct_info")] + class TypeInfoGetStructInfo : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; + var errors : das_string) : ExpressionPtr { + if (expr.typeexpr == null) { + errors := "type is missing or not inferred" + return <- default + } + if (!expr.typeexpr.isStructure) { + errors := "expecting structure type" + return <- default + } + var result = "{expr.typeexpr.structType.name}(" + var first = true + for (i in iter_range(expr.typeexpr.structType.fields)) { + assume fld = expr.typeexpr.structType.fields[i] + if (fld.flags.classMethod) { + continue + } + if (!first) { + result += ", " + } + result += "{fld.name}:{describe(fld._type, false, false, false)}" + first = false + } + result += ")" + return <- new ExprConstString(at = expr.at, value := result) + } + } + +Key points: + +- ``expr.typeexpr.isStructure`` validates that the type is a structure. +- ``expr.typeexpr.structType.fields`` iterates all field declarations. +- ``fld.flags.classMethod`` skips class methods (only data fields appear). +- ``describe(fld._type, ...)`` converts a ``TypeDeclPtr`` to a human-readable + type name. +- The result is an ``ExprConstString`` — a compile-time string constant. + + +enum_value_strings — returning an array +---------------------------------------- + +``typeinfo enum_value_strings(type)`` returns a fixed-size array of +enum value names: + +.. code-block:: das + + [typeinfo_macro(name="enum_value_strings")] + class TypeInfoGetEnumValueStrings : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; + var errors : das_string) : ExpressionPtr { + // ... validation ... + var inscope arr <- new ExprMakeArray( + at = expr.at, + makeType <- typeinfo ast_typedecl(type)) + for (i in iter_range(expr.typeexpr.enumType.list)) { + if (true) { + assume entry = expr.typeexpr.enumType.list[i] + var inscope nameExpr <- new ExprConstString( + at = expr.at, value := entry.name) + arr.values |> emplace <| nameExpr + } + } + return <- arr + } + } + +Key points: + +- ``ExprMakeArray`` requires a ``makeType`` field — use ``typeinfo + ast_typedecl(type)`` to get the AST representation of + ``string``. +- ``expr.typeexpr.enumType.list`` iterates all ``EnumEntry`` nodes. +- The ``if (true)`` block is needed for ``var inscope`` lifetime scoping + (each loop iteration creates and consumes a new ``inscope`` smart pointer). +- The result is a **fixed-size array** (``string[N]``), not a dynamic + ``array``. + + +has_non_static_method — returning a bool with subtrait +------------------------------------------------------ + +``typeinfo has_non_static_method(type)`` checks whether a class +has a non-static method with the given name. The method name is passed +via the ``subtrait`` parameter: + +.. code-block:: das + + [typeinfo_macro(name="has_non_static_method")] + class TypeInfoHasNonStaticMethod : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; + var errors : das_string) : ExpressionPtr { + // ... validation ... + if (empty(expr.subtrait)) { + errors := "expecting method name as subtrait" + return <- default + } + var found = false + for (i in iter_range(expr.typeexpr.structType.fields)) { + assume fld = expr.typeexpr.structType.fields[i] + if (fld.name == expr.subtrait && fld.flags.classMethod) { + found = true + break + } + } + return <- new ExprConstBool(at = expr.at, value = found) + } + } + +Key points: + +- ``expr.subtrait`` is the string between angle brackets — in + ``typeinfo has_non_static_method(type)``, ``subtrait`` is + ``"speak"``. +- Class methods are stored as struct fields with the ``classMethod`` flag. +- The result is ``ExprConstBool`` — a compile-time boolean, perfect for + use in ``static_if`` conditional compilation. + + +The usage file +============== + +Full source: :download:`12_typeinfo_macro.das <../../../../../tutorials/macros/12_typeinfo_macro.das>` + + +Section 1: struct_info +---------------------- + +.. code-block:: das + + struct Vec3 { + x : float + y : float + z : float + } + + struct Person { + name : string + age : int + } + + def section1() { + let v = typeinfo struct_info(type) + print(" Vec3: {v}\n") + let p = typeinfo struct_info(type) + print(" Person: {p}\n") + } + +``typeinfo struct_info(type)`` is replaced at compile time with +the constant string ``"Vec3(x:float, y:float, z:float)"``. No runtime +reflection is involved. + + +Section 2: enum_value_strings +----------------------------- + +.. code-block:: das + + enum Color { + Red + Green + Blue + } + + def section2() { + let colors = typeinfo enum_value_strings(type) + for (c in colors) { + print(" {c}\n") + } + } + +The macro generates a fixed-size ``string[3]`` array containing +``"Red"``, ``"Green"``, ``"Blue"``. Use ``let`` (not ``var <-``) to +bind the result — fixed-size arrays are value types. + + +Section 3: has_non_static_method +-------------------------------- + +.. code-block:: das + + class Animal { + name : string + def speak() { + print(" {name} says hello\n") + } + } + + class Rock { + weight : float + } + + def section3() { + let animal_can_speak = typeinfo has_non_static_method(type) + let rock_can_speak = typeinfo has_non_static_method(type) + static_if (typeinfo has_non_static_method(type)) { + print(" (static_if confirmed: Animal can speak)\n") + } + } + +The subtrait makes typeinfo macros parametric — the same macro handles +any method name. Since the result is a compile-time ``bool``, it works +directly in ``static_if`` for conditional code generation. + + +Output +====== + +.. code-block:: text + + --- Section 1: struct_info --- + Vec3: Vec3(x:float, y:float, z:float) + Person: Person(name:string, age:int) + --- Section 2: enum_value_strings --- + Color values (3): + Red + Green + Blue + Direction values (4): + North + South + East + West + --- Section 3: has_non_static_method --- + Animal has 'speak': true + Animal has 'fly': false + Rock has 'speak': false + (static_if confirmed: Animal can speak) + + +Real-world usage +================ + +The standard library provides several typeinfo macros: + +- ``typeinfo fields_count(type)`` — number of struct fields + (``daslib/type_traits.das``) +- ``typeinfo safe_has_property(expr)`` — does a type have a + property function? (``daslib/type_traits.das``) +- ``typeinfo enum_length(type)`` — number of enum values + (``daslib/enum_trait.das``) +- ``typeinfo enum_names(type)`` — array of enum value name strings + (``daslib/enum_trait.das``) + + +.. seealso:: + + Full source: + :download:`12_typeinfo_macro.das <../../../../../tutorials/macros/12_typeinfo_macro.das>`, + :download:`typeinfo_macro_mod.das <../../../../../tutorials/macros/typeinfo_macro_mod.das>` + + Previous tutorial: :ref:`tutorial_macro_reader_macro` + + Next tutorial: :ref:`tutorial_macro_enumeration_macro` + + Standard library: ``daslib/type_traits.das``, ``daslib/enum_trait.das`` + + Language reference: :ref:`Macros ` — full macro system documentation diff --git a/doc/source/reference/tutorials/macros/13_enumeration_macro.rst b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst new file mode 100644 index 0000000000..8aa9829606 --- /dev/null +++ b/doc/source/reference/tutorials/macros/13_enumeration_macro.rst @@ -0,0 +1,305 @@ +.. _tutorial_macro_enumeration_macro: + +.. index:: + single: Tutorial; Macros; Enumeration Macro + single: Tutorial; Macros; enumeration_macro + single: Tutorial; Macros; AstEnumerationAnnotation + single: Tutorial; Macros; enum_total + single: Tutorial; Macros; string_to_enum + single: Tutorial; Macros; enum_trait + +==================================================== + Macro Tutorial 13: Enumeration Macros +==================================================== + +Previous tutorials transformed calls, functions, structures, blocks, +variants, for-loops, lambda captures, reader macros, and typeinfo +expressions. Enumeration macros let you intercept enum declarations +at compile time and either **modify the enum** or **generate new code**. + +``[enumeration_macro(name="X")]`` registers a class that extends +``AstEnumerationAnnotation``. This is the simplest macro type — it has +only one method: + +``apply(var enu, var group, args, var errors) → bool`` + Called **before the infer pass**. The macro receives the full + ``EnumerationPtr`` and can add/remove entries, generate functions, or + create global variables. Return ``true`` on success, ``false`` with + an error message to abort compilation. + + +Motivation +========== + +Enumerations in daScript are simple value lists. But real-world code +often needs derived information: + +- **A "total" sentinel** — the number of values, for array sizing or + range checking. +- **String constructors** — converting user input strings to enum values + at runtime. + +Both can be achieved with enumeration macros: + +- Section 1 shows ``[enum_total]`` — a custom macro that *modifies* an + enum by appending a ``total`` entry. +- Section 2 shows ``[string_to_enum]`` from ``daslib/enum_trait`` — a + standard library macro that *generates code* (a lookup table and two + constructor functions). + + +The module file — enum_total +============================ + +The macro module defines a single ``AstEnumerationAnnotation`` subclass +that adds a ``total`` entry to any enum. + +Full source: :download:`enum_macro_mod.das <../../../../../tutorials/macros/enum_macro_mod.das>` + +.. code-block:: das + + [enumeration_macro(name="enum_total")] + class EnumTotalAnnotation : AstEnumerationAnnotation { + def override apply(var enu : EnumerationPtr; + var group : ModuleGroup; + args : AnnotationArgumentList; + var errors : das_string) : bool { + // Check that the enum doesn't already have a "total" entry. + for (ee in enu.list) { + if (ee.name == "total") { + errors := "enumeration already has a 'total' field" + return false + } + } + // Add a new "total" entry at the end. + let idx = add_enumeration_entry(enu, "total") + if (idx < 0) { + errors := "failed to add 'total' field" + return false + } + // Set total = number of original entries. + // length(enu.list) is now N+1, so total = N+1-1 = N. + enu.list[idx].value |> move_new() <| new ExprConstInt( + at = enu.at, value = length(enu.list) - 1) + return true + } + } + +Key points: + +- ``enu.list`` is the array of ``EnumEntry`` nodes — iterate it to + validate existing entries. +- ``add_enumeration_entry(enu, "total")`` appends a new entry and + returns its index (or ``-1`` on failure). +- ``enu.list[idx].value`` is an ``ExpressionPtr`` — use ``move_new`` + to assign a new ``ExprConstInt`` with the desired integer value. +- ``enu.at`` provides the source location for the generated expression. +- Return ``false`` with an error message to abort compilation — the + error will point to the enum declaration. + + +The usage file +============== + +Full source: :download:`13_enumeration_macro.das <../../../../../tutorials/macros/13_enumeration_macro.das>` + + +Section 1 — enum_total (modifying the enum) +-------------------------------------------- + +.. code-block:: das + + require enum_macro_mod + + [enum_total] + enum Direction { + North + South + East + West + } + + def section1() { + print("--- Section 1: enum_total ---\n") + print("Direction.total = {int(Direction.total)}\n") + for (d in each(Direction.North)) { + if (d == Direction.total) { + break + } + print(" {d}\n") + } + } + +The ``[enum_total]`` annotation runs before type inference and appends +``total = 4`` to the enum. At compile time the enum becomes: + +.. code-block:: das + + enum Direction { + North = 0 + South = 1 + East = 2 + West = 3 + total = 4 + } + +The ``each()`` function from ``daslib/enum_trait`` iterates all values +(including ``total``), so the loop breaks when it reaches the sentinel. + + +Section 2 — string_to_enum (generating code) +--------------------------------------------- + +.. code-block:: das + + require daslib/enum_trait + + [string_to_enum] + enum Color { + Red + Green + Blue + } + + def section2() { + print("--- Section 2: string_to_enum ---\n") + let c1 = Color("Red") + print("Color(\"Red\") = {c1}\n") + let c2 = Color("invalid", Color.Blue) + print("Color(\"invalid\", Color.Blue) = {c2}\n") + } + +The ``[string_to_enum]`` annotation from ``daslib/enum_trait`` generates +three things at compile time: + +1. A **private global table** ``_`enum`table`Color`` mapping strings to + enum values — created with ``add_global_private_let`` and + ``enum_to_table``. + +2. A **single-argument constructor** ``Color(src : string) : Color`` — + panics if the string is not a valid enum name. + +3. A **two-argument constructor** ``Color(src : string; defaultValue : Color) : Color`` — + returns ``defaultValue`` if the string is not found. + +Both constructors are generated with ``qmacro_function``, registered +with ``add_function(compiling_module(), fn)``, and marked as +non-private with ``enumFn.flags &= ~FunctionFlags.privateFunction``. + + +How string_to_enum works internally +==================================== + +The ``EnumFromStringConstruction`` class in ``daslib/enum_trait.das`` +demonstrates the **code generation** pattern for enumeration macros: + +.. code-block:: das + + [enumeration_macro(name="string_to_enum")] + class EnumFromStringConstruction : AstEnumerationAnnotation { + def override apply(var enu : EnumerationPtr; + var group : ModuleGroup; + args : AnnotationArgumentList; + var errors : das_string) : bool { + var inscope enumT <- new TypeDecl( + baseType = Type.tEnumeration, + enumType = enu.get_ptr()) + // 1. Create a private global lookup table. + let varName = "_`enum`table`{enu.name}" + add_global_private_let( + compiling_module(), varName, enu.at, + qmacro(enum_to_table(type<$t(enumT)>))) + // 2. Generate the panic-on-miss constructor. + var inscope enumFn <- qmacro_function("{enu.name}") + $(src : string) : $t(enumT) { + if (!key_exists($i(varName), src)) { + panic("enum value '{src}' not found") + } + return $i(varName)?[src] ?? default<$t(enumT)> + } + enumFn.flags &= ~FunctionFlags.privateFunction + force_at(enumFn, enu.at) + force_generated(enumFn, true) + compiling_module() |> add_function(enumFn) + // 3. Generate the default-on-miss constructor. + var inscope enumFnDefault <- qmacro_function("{enu.name}") + $(src : string; defaultValue : $t(enumT)) : $t(enumT) { + return $i(varName)?[src] ?? defaultValue + } + enumFnDefault.flags &= ~FunctionFlags.privateFunction + force_at(enumFnDefault, enu.at) + force_generated(enumFnDefault, true) + compiling_module() |> add_function(enumFnDefault) + return true + } + } + +Key code-generation techniques: + +- **``qmacro_function("name") $(args) : ReturnType { body }``** — + creates a new function AST node. Reification splices (``$t()``, + ``$i()``) inject types and identifiers from variables. +- **``add_global_private_let(module, name, at, expr)``** — adds a + private global ``let`` variable initialized by ``expr``. +- **``compiling_module()``** — returns the module being compiled (where + the annotated enum lives), so generated functions appear in the + user's module. +- **``force_at(fn, at)``** — sets the source location of all nodes in + the generated function to ``at``, so error messages point to the + enum declaration. +- **``force_generated(fn, true)``** — marks the function as + compiler-generated (suppresses "unused function" warnings). + + +Output +====== + +.. code-block:: text + + --- Section 1: enum_total --- + Direction.total = 4 + North + South + East + West + --- Section 2: string_to_enum --- + Color("Red") = Red + Color("invalid", Color.Blue) = Blue + + +Real-world usage +================ + +``daslib/enum_trait`` provides a rich set of enumeration utilities: + +- ``[string_to_enum]`` — generates string constructors (shown above) +- ``each(enumValue)`` — iterates over all values of an enum type +- ``string(enumValue)`` — converts an enum value to its name +- ``to_enum(type, "name")`` — runtime string-to-enum conversion +- ``enum_to_table(type)`` — creates a ``table`` lookup +- ``typeinfo enum_length(type)`` — compile-time count of enum values +- ``typeinfo enum_names(type)`` — compile-time array of value names + +The two patterns shown in this tutorial cover the majority of +enumeration macro use cases: + +- **Modify the enum** — add sentinel values, computed entries, or + validation (like ``[enum_total]``). +- **Generate code** — create functions, tables, or variables derived + from the enum's structure (like ``[string_to_enum]``). + + +.. seealso:: + + Full source: + :download:`13_enumeration_macro.das <../../../../../tutorials/macros/13_enumeration_macro.das>`, + :download:`enum_macro_mod.das <../../../../../tutorials/macros/enum_macro_mod.das>` + + Previous tutorial: :ref:`tutorial_macro_typeinfo_macro` + + Next tutorial: :ref:`tutorial_macro_pass_macro` + + Standard library: ``daslib/enum_trait.das`` — + :ref:`enum_trait module reference ` + + Language reference: :ref:`Macros ` — full macro system documentation diff --git a/doc/source/reference/tutorials/macros/14_pass_macro.rst b/doc/source/reference/tutorials/macros/14_pass_macro.rst new file mode 100644 index 0000000000..cb47c66d6c --- /dev/null +++ b/doc/source/reference/tutorials/macros/14_pass_macro.rst @@ -0,0 +1,359 @@ +.. _tutorial_macro_pass_macro: + +.. index:: + single: Tutorial; Macros; Pass Macro + single: Tutorial; Macros; pass_macro + single: Tutorial; Macros; AstPassMacro + single: Tutorial; Macros; lint_macro + single: Tutorial; Macros; infer_macro + +==================================================== + Macro Tutorial 14: Pass Macro +==================================================== + +Previous tutorials showed macros attached to specific language +constructs — calls, functions, structures, blocks, loops, enums, and +typeinfo expressions. **Pass macros** operate at a higher level: they +receive the **entire program** and run during a specific compilation +phase. + +``AstPassMacro`` is the base class for all pass macros. It has a +single method: + +``apply(prog : ProgramPtr; mod : Module?) → bool`` + ``prog`` is the full program being compiled. + ``mod`` is the module that registered the macro. + The return value depends on the annotation (see below). + +Five annotations control **when** the macro runs: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Annotation + - Behaviour + * - ``[infer_macro]`` + - Runs after clean **type inference**. Returning ``true`` means + "I changed something" — the compiler re-infers and runs macros + again. The loop repeats until every macro returns ``false``. + * - ``[dirty_infer_macro]`` + - Runs during each **dirty inference** pass (the AST may be + half-resolved). All dirty macros fire on every pass. + * - ``[lint_macro]`` + - Invoked for each module compiled after the macro module, + during the **lint phase**. Read-only — use it for analysis and + diagnostics. Use ``compiling_module()`` to get the module + currently being compiled. + * - ``[global_lint_macro]`` + - Same as ``[lint_macro]`` but runs for **all** modules, not just + the one that requires it. + * - ``[optimization_macro]`` + - Runs during the **optimization** loop, after built-in + optimisations. Returning ``true`` continues the loop. + +This tutorial demonstrates the two most common types: + +- **Section 1** — ``[lint_macro]``: compile-time analysis + (``CodeStatsLint``). +- **Section 2** — ``[infer_macro]``: AST transformation + (``TraceCallsPass``). + + +The module file +=============== + +Full source: :download:`pass_macro_mod.das <../../../../../tutorials/macros/pass_macro_mod.das>` + +Both macros live in a single module that the user requires. + + +Section 1 — lint_macro (compile-time analysis) +---------------------------------------------- + +.. code-block:: das + + [lint_macro] + class CodeStatsLint : AstPassMacro { + def override apply(prog : ProgramPtr; mod : Module?) : bool { + let WARN_THRESHOLD = 4 + get_ptr(prog) |> for_each_module() $(var m : Module?) { + if (m.moduleFlags.builtIn) { + return // skip C++ built-in modules + } + m |> for_each_function("") <| $(var func : FunctionPtr) { + if (func.body == null || !(func.body is ExprBlock)) { + return + } + let body = func.body as ExprBlock + let nStmts = length(body.list) + if (nStmts > WARN_THRESHOLD) { + print("[lint] '{func.name}' has {nStmts} statements (>{WARN_THRESHOLD})\n") + } + } + } + return false // lint macros don't modify the AST + } + } + +Key points: + +- ``[lint_macro]`` means this class runs **after inference succeeds**, + during the read-only lint phase. +- ``get_ptr(prog) |> for_each_module()`` walks the **entire compiled + program**. ``m.moduleFlags.builtIn`` skips C++ built-in modules so + only user code is inspected. +- ``for_each_function("")`` iterates each module's functions. The + empty string means "all names". +- The lint checks function body size: any function with more than + ``WARN_THRESHOLD`` top-level statements triggers a compile-time + warning via ``print``. +- ``print(...)`` outputs at **compile time** — the message appears + before any runtime output. +- The return value of a lint macro is ignored; lint macros never trigger + re-inference. +- In production code, use ``compiling_program() |> macro_error(at, + text)`` to emit **real compiler errors** instead of ``print``. + See ``daslib/lint.das`` for a full example. + + +Section 2 — infer_macro (AST transformation) +--------------------------------------------- + +The infer macro instruments every function in the program by inserting a +``_trace_enter("function_name")`` call at the start of each function +body. It follows the same visitor pattern as ``daslib/heartbeat.das``. + +First, a helper function that the injected code will call: + +.. code-block:: das + + def public _trace_enter(name : string) { + print(">>> {name}\n") + } + +The visitor walks the AST and modifies function bodies: + +.. code-block:: das + + class TraceCallsVisitor : AstVisitor { + astChanged : bool = false + @do_not_delete func : Function? + def override preVisitFunction(var fun : FunctionPtr) { + func = get_ptr(fun) + } + def override visitFunction(var fun : FunctionPtr) : FunctionPtr { + // Skip our own helper to avoid infinite recursion at runtime. + if (string(fun.name) == "_trace_enter") { + func = null + return <- fun + } + if (fun.body == null || !(fun.body is ExprBlock)) { + func = null + return <- fun + } + var body = fun.body as ExprBlock + if (length(body.list) == 0) { + func = null + return <- fun + } + // Idempotency: skip if already instrumented. + if ((body.list[0] is ExprCall) && + (body.list[0] as ExprCall).name == "_trace_enter") { + func = null + return <- fun + } + // Insert _trace_enter("function_name") at the beginning. + let fname = string(fun.name) + var inscope expr <- qmacro(_trace_enter($v(fname))) + body.list |> emplace(expr, 0) + astChanged = true + func.not_inferred() + func = null + return <- fun + } + } + +Key visitor techniques: + +- **``preVisitFunction``** captures a raw pointer (``@do_not_delete + func : Function?``) to the function being visited. +- **``visitFunction``** fires *after* the function body has been visited. + It returns a ``FunctionPtr`` — returning ``<- fun`` keeps the + original unchanged. +- **Self-exclusion** — ``_trace_enter`` must not instrument itself, or + it would cause infinite recursion at runtime. +- **Idempotency** — checking whether the first statement is already a + ``_trace_enter`` call prevents the macro from modifying the + same function again on re-inference. +- **``qmacro(_trace_enter($v(fname)))``** generates a call expression. + ``$v(fname)`` splices the string value of ``fname`` as a constant. +- **``body.list |> emplace(expr, 0)``** inserts the expression at + position 0 (the start of the function body). +- **``func.not_inferred()``** marks the function as needing + re-inference, so the compiler processes the injected call. +- **``astChanged = true``** signals that the pass made modifications. + +The pass macro creates the visitor and walks the full program: + +.. code-block:: das + + [infer_macro] + class TraceCallsPass : AstPassMacro { + def override apply(prog : ProgramPtr; mod : Module?) : bool { + var astVisitor = new TraceCallsVisitor() + var inscope astVisitorAdapter <- make_visitor(*astVisitor) + visit(prog, astVisitorAdapter) + var result = astVisitor.astChanged + unsafe { + delete astVisitor + } + return result + } + } + +Key points: + +- **``new TraceCallsVisitor()``** allocates the visitor on the heap + (macro code cannot use stack-allocated visitors). +- **``make_visitor(*astVisitor)``** wraps it in a ``smart_ptr`` + adapter for the ``visit`` function. +- **``visit(prog, astVisitorAdapter)``** walks the entire program — all + modules, all functions. Use ``visit(func, adapter)`` to walk a + single function instead. +- **Return value** — ``true`` tells the compiler to re-infer. Since + the idempotency check prevents double-instrumentation, the second + pass returns ``false`` and inference stabilises. +- **``unsafe { delete astVisitor }``** cleans up the raw pointer. The + smart-ptr adapter is destroyed by ``var inscope``. + + +The usage file +============== + +Full source: :download:`14_pass_macro.das <../../../../../tutorials/macros/14_pass_macro.das>` + +.. code-block:: das + + require pass_macro_mod + + def greet(name : string) { + print("Hello, {name}!\n") + } + + def sum_to(n : int) : int { + var total = 0 + for (i in range(n)) { + total += i + } + return total + } + + def countdown(n : int) { + var i = n + while (i > 0) { + i-- + } + print("counted down from {n}\n") + } + + [export] + def main() { + greet("world") + let s = sum_to(5) + print("sum = {s}\n") + countdown(3) + } + +The user code contains no trace calls — the ``[infer_macro]`` injects +them automatically. The ``[lint_macro]`` prints its compile-time +message before any runtime output. + + +Output +====== + +.. code-block:: text + + [lint] 'main' has 5 top-level statements (>4) + >>> main + >>> greet + Hello, world! + >>> sum_to + sum = 10 + >>> countdown + counted down from 3 + +The first line is the lint macro's compile-time message — when linting +the user module, it found that ``main`` has 5 top-level statements (the +infer macro already added a ``_trace_enter`` call, raising its count +above the threshold). The ``>>>`` lines come from the injected +``_trace_enter`` calls, proving that every user function was +instrumented at compile time. + + +How it works — compilation pipeline +==================================== + +When the compiler processes the user's program: + +1. **Parsing** — the source is parsed into an AST. +2. **Dirty inference** — ``[dirty_infer_macro]`` macros run on each + pass (not used in this tutorial). +3. **Clean inference** — type inference runs. After it succeeds, + ``[infer_macro]`` macros execute. ``TraceCallsPass`` instruments + functions and returns ``true``. The compiler re-infers. On the + second pass, no new changes are made, so it returns ``false``. +4. **Optimisation** — ``[optimization_macro]`` macros run in the + optimisation loop (not used here). +5. **Lint** — ``[lint_macro]`` macros run for each module compiled + after the macro module. ``CodeStatsLint`` inspects the user module + via ``compiling_module()`` and warns about large function bodies. + (``[global_lint_macro]`` macros run once for the entire program.) +6. **Execution** — the instrumented program runs. + + +Avoiding infinite loops +======================= + +An infer macro that always returns ``true`` will cause the compiler to +hit its maximum pass limit and report an error. Two techniques prevent +this: + +- **Idempotency check** — before modifying a function, verify that the + modification hasn't already been applied (e.g. check whether the + first statement is already the injected call). +- **Targeted modification** — only modify functions that match specific + criteria (e.g. skip ``_trace_enter`` to avoid self-instrumentation). + + +Real-world examples +=================== + +The standard library uses pass macros for several purposes: + +- **``daslib/heartbeat.das``** — ``[infer_macro]`` that inserts a + ``heartbeat()`` call at every function and loop entry, enabling + cooperative multitasking for long-running scripts. +- **``daslib/coverage.das``** — ``[infer_macro]`` that instruments every + block with coverage-tracking calls. +- **``daslib/lint.das``** — ``[lint_macro]`` that enables paranoid + compilation checks (unreachable code, unused variables, const + upgrades, etc.). +- **``daslib/lint_everything.das``** — ``[global_lint_macro]`` that + applies paranoid checks to **all** modules, not just the one that + requires it. + + +.. seealso:: + + Full source: + :download:`14_pass_macro.das <../../../../../tutorials/macros/14_pass_macro.das>`, + :download:`pass_macro_mod.das <../../../../../tutorials/macros/pass_macro_mod.das>` + + Previous tutorial: :ref:`tutorial_macro_enumeration_macro` + + Standard library: ``daslib/heartbeat.das``, ``daslib/coverage.das``, + ``daslib/lint.das`` + + Language reference: :ref:`Macros ` — full macro system documentation diff --git a/include/daScript/ast/ast.h b/include/daScript/ast/ast.h index 324788fc19..2c71363c08 100644 --- a/include/daScript/ast/ast.h +++ b/include/daScript/ast/ast.h @@ -1413,6 +1413,7 @@ namespace das CaptureMacro ( const string & na = "" ) : name(na) {} virtual ExpressionPtr captureExpression ( Program *, Module *, Expression *, TypeDecl * ) { return nullptr; } virtual void captureFunction ( Program *, Module *, Structure *, Function * ) { } + virtual void releaseFunction ( Program *, Module *, Structure *, Function * ) { } string name; }; diff --git a/include/daScript/ast/ast_generate.h b/include/daScript/ast/ast_generate.h index 3234d3a64d..4a28d926a6 100644 --- a/include/daScript/ast/ast_generate.h +++ b/include/daScript/ast/ast_generate.h @@ -206,7 +206,7 @@ namespace das { ...block_finally... */ DAS_API FunctionPtr generateLambdaFinalizer ( const string & lambdaName, ExprBlock * block, - const StructurePtr & ls ); + const StructurePtr & ls, Program * thisProgram ); /* [[__lambda_at_line_xxx diff --git a/include/daScript/builtin/ast_gen.inc b/include/daScript/builtin/ast_gen.inc index 0072b7540e..01db60aafe 100644 --- a/include/daScript/builtin/ast_gen.inc +++ b/include/daScript/builtin/ast_gen.inc @@ -377,13 +377,15 @@ protected: enum { __fn_captureExpression = 0, __fn_captureFunction = 1, + __fn_releaseFunction = 2, }; protected: - int _das_class_method_offset[2]; + int _das_class_method_offset[3]; public: AstCaptureMacro_Adapter ( const StructInfo * info ) { _das_class_method_offset[__fn_captureExpression] = info->fields[2]->offset; _das_class_method_offset[__fn_captureFunction] = info->fields[3]->offset; + _das_class_method_offset[__fn_releaseFunction] = info->fields[4]->offset; } __forceinline Func get_captureExpression ( void * self ) const { return getDasClassMethod(self,_das_class_method_offset[__fn_captureExpression]); @@ -403,6 +405,15 @@ public: (__context__,nullptr,__funcCall__, self,prog,mod,lcs,fun); } + __forceinline Func get_releaseFunction ( void * self ) const { + return getDasClassMethod(self,_das_class_method_offset[__fn_releaseFunction]); + } + __forceinline void invoke_releaseFunction ( Context * __context__, Func __funcCall__, void * self, Program * const prog, Module * const mod, Structure * lcs, smart_ptr_raw fun ) const { + das_invoke_function::invoke + > + (__context__,nullptr,__funcCall__, + self,prog,mod,lcs,fun); + } }; class AstTypeMacro_Adapter { diff --git a/src/ast/ast_generate.cpp b/src/ast/ast_generate.cpp index ecbdd468b8..760b6ca892 100644 --- a/src/ast/ast_generate.cpp +++ b/src/ast/ast_generate.cpp @@ -511,7 +511,7 @@ namespace das { } FunctionPtr generateLambdaFinalizer ( const string & lambdaName, ExprBlock * block, - const StructurePtr & ls ) { + const StructurePtr & ls, Program * thisProgram ) { auto lfn = lambdaName + "`finalizer"; auto pFunc = make_smart(); pFunc->privateFunction = true; @@ -536,6 +536,15 @@ namespace das { } fb->list.push_back(with); } + // now, lets generate all release functions (after the original finally section is generated, but before deleting the lambda itself) + pFunc->body = fb; + thisProgram->library.foreach([&](Module * mod){ + for ( auto & cm : mod->captureMacros ) { + cm->releaseFunction(thisProgram, thisProgram->thisModule.get(), ls.get(), pFunc.get()); + } + return true; + },"*"); + // delete * this auto THISA = make_smart(block->at, "__this"); auto THISAP = make_smart(block->at, THISA); @@ -548,7 +557,7 @@ namespace das { delit1->native = true; delit1->alwaysSafe = true; fb->list.push_back(delit1); - pFunc->body = fb; + // function goo pFunc->result = make_smart(Type::tVoid); auto cTHIS = make_smart(); cTHIS->at = ls->at; diff --git a/src/ast/ast_infer_type_make.cpp b/src/ast/ast_infer_type_make.cpp index b4e83cf360..6f8f7e38dc 100644 --- a/src/ast/ast_infer_type_make.cpp +++ b/src/ast/ast_infer_type_make.cpp @@ -116,7 +116,7 @@ namespace das { if (func && func->skipLockCheck) pFn->skipLockCheck = true; // we propagate skipLockCheck to the generator function if (program->addFunction(pFn)) { - auto pFnFin = generateLambdaFinalizer(lname, block.get(), ls); + auto pFnFin = generateLambdaFinalizer(lname, block.get(), ls, program); if (program->addFunction(pFnFin)) { if (func && func->isClassMethod) { // lambda, captured in the class is a method of that class - for the purposes of 'private' @@ -246,7 +246,7 @@ namespace das { if (func && func->skipLockCheck) pFn->skipLockCheck = true; // we propagate skipLockCheck to the lambda function if (program->addFunction(pFn)) { - auto pFnFin = generateLambdaFinalizer(lname, block.get(), ls); + auto pFnFin = generateLambdaFinalizer(lname, block.get(), ls, program); if (program->addFunction(pFnFin)) { // lambda, captured in the class is a method of that class - for the purposes of 'private' if (func && func->isClassMethod) { diff --git a/src/builtin/ast.das b/src/builtin/ast.das index 6bac5fff4a..6e939de90b 100644 --- a/src/builtin/ast.das +++ b/src/builtin/ast.das @@ -133,6 +133,7 @@ class AstForLoopMacro { class AstCaptureMacro { def abstract captureExpression(prog : Program?; mod : Module?; expr : ExpressionPtr; etype : TypeDeclPtr) : ExpressionPtr def abstract captureFunction(prog : Program?; mod : Module?; var lcs : Structure?; var fun : FunctionPtr) : void + def abstract releaseFunction(prog : Program?; mod : Module?; var lcs : Structure?; var fun : FunctionPtr) : void } [macro_interface] diff --git a/src/builtin/ast.das.inc b/src/builtin/ast.das.inc index 6705d581f7..74b2b8af0d 100644 --- a/src/builtin/ast.das.inc +++ b/src/builtin/ast.das.inc @@ -874,6 +874,21 @@ static unsigned char ast_das[] = { 0x20,0x46,0x75,0x6e,0x63,0x74,0x69,0x6f, 0x6e,0x50,0x74,0x72,0x29,0x20,0x3a,0x20, 0x76,0x6f,0x69,0x64,0x0a, +0x20,0x20,0x20,0x20,0x64,0x65,0x66,0x20, +0x61,0x62,0x73,0x74,0x72,0x61,0x63,0x74, +0x20,0x72,0x65,0x6c,0x65,0x61,0x73,0x65, +0x46,0x75,0x6e,0x63,0x74,0x69,0x6f,0x6e, +0x28,0x70,0x72,0x6f,0x67,0x20,0x3a,0x20, +0x50,0x72,0x6f,0x67,0x72,0x61,0x6d,0x3f, +0x3b,0x20,0x6d,0x6f,0x64,0x20,0x3a,0x20, +0x4d,0x6f,0x64,0x75,0x6c,0x65,0x3f,0x3b, +0x20,0x76,0x61,0x72,0x20,0x6c,0x63,0x73, +0x20,0x3a,0x20,0x53,0x74,0x72,0x75,0x63, +0x74,0x75,0x72,0x65,0x3f,0x3b,0x20,0x76, +0x61,0x72,0x20,0x66,0x75,0x6e,0x20,0x3a, +0x20,0x46,0x75,0x6e,0x63,0x74,0x69,0x6f, +0x6e,0x50,0x74,0x72,0x29,0x20,0x3a,0x20, +0x76,0x6f,0x69,0x64,0x0a, 0x7d,0x0a, 0x0a, 0x5b,0x6d,0x61,0x63,0x72,0x6f,0x5f,0x69, diff --git a/src/builtin/module_builtin_ast_adapters.cpp b/src/builtin/module_builtin_ast_adapters.cpp index 0640758a0d..4edf2f55d0 100644 --- a/src/builtin/module_builtin_ast_adapters.cpp +++ b/src/builtin/module_builtin_ast_adapters.cpp @@ -1585,6 +1585,13 @@ namespace das { }); } } + virtual void releaseFunction ( Program * prog, Module * mod, Structure * lcs, Function * fun ) override { + if ( auto fnReleaseFunction = get_releaseFunction(classPtr) ) { + runMacroFunction(context, "releaseFunction", [&]() { + invoke_releaseFunction(context,fnReleaseFunction,classPtr,prog,mod,lcs,fun); + }); + } + } protected: void * classPtr; Context * context; diff --git a/tutorials/language/14_lambdas.das b/tutorials/language/14_lambdas.das index 37a8274267..8b7aac9952 100644 --- a/tutorials/language/14_lambdas.das +++ b/tutorials/language/14_lambdas.das @@ -8,6 +8,8 @@ // - Storing lambdas in variables (move semantics) // - Simplified => syntax // - Lambda vs block vs function +// - Finally blocks (cleanup on lambda destruction) +// - Lambda lifecycle: capture → invocations → destruction (finally + field cleanup) // // Run: daslang.exe tutorials/language/14_lambdas.das @@ -137,6 +139,64 @@ def main { } var c1 <- invoke(make_counter) print("c1: {c1()}, {c1()}, {c1()}\n") // 1, 2, 3 + + // === Lambda lifecycle === + // A lambda is a heap-allocated struct with fields for each captured variable. + // The full lifecycle is: + // 1. CAPTURE — variables are copied/moved/cloned into the lambda struct + // 2. INVOKE — the lambda body runs (may be called many times) + // 3. DESTROY — when the lambda is deleted or GC'd: + // a) finally{} block runs (user cleanup code) + // b) captured fields are finalized (compiler-generated delete) + // c) the struct memory is freed + // + // NOTE: lambda finally{} runs during DESTRUCTION, not after each call. + // This is different from block finally{}, which runs after each invocation. + + // === Captured field cleanup === + // Captured fields are automatically deleted when the lambda is destroyed, + // UNLESS: + // - The field was captured by reference (pointer to external data — not owned) + // - The field was captured by move or clone (doNotDelete flag is set) + // - The field type is POD (int, float, etc. — no cleanup needed) + // + // Example: an array captured by clone. The original stays intact; + // the cloned copy inside the lambda is cleaned up on destruction. + print("\n--- captured field cleanup ---\n") + var nums <- [100, 200, 300] + var sum_nums <- @capture(clone(nums)) () : int { + var s = 0 + for (v in nums) { + s += v + } + return s + } + print("sum = {sum_nums()}\n") // 600 + print("original nums = {nums}\n") // still [100, 200, 300] + // When sum_nums is deleted or goes out of scope, the cloned array + // inside the lambda struct is automatically finalized. + + // === Finally blocks on lambdas === + // A lambda's finally{} block runs ONCE when the lambda is DESTROYED + // (deleted or garbage collected) — NOT after each invocation. + // + // This is the opposite of block finally{}, which runs after every call. + // + // Use lambda finally{} for one-time destruction cleanup, such as + // releasing resources that the lambda owns. + print("\n--- finally on destruction ---\n") + var data2 <- [10, 20] + var demo <- @capture(clone(data2)) () { + print(" body: data2 has {length(data2)} items\n") + } finally { + // This runs once, when 'demo' is destroyed. + print(" finally: lambda destroyed\n") + } + demo() + demo() + print(" about to delete demo...\n") + unsafe { delete demo; } + // Output shows: body twice, then "about to delete", then finally once. } def apply_lambda(fn : lambda<(x : int) : int>; value : int) : int { @@ -162,3 +222,13 @@ def apply_lambda(fn : lambda<(x : int) : int>; value : int) : int { // cloned items count = 3 // original items still = 3 // c1: 1, 2, 3 +// +// --- captured field cleanup --- +// sum = 600 +// original nums = [[ 100; 200; 300]] +// +// --- finally on destruction --- +// body: data2 has 2 items +// body: data2 has 2 items +// about to delete demo... +// finally: lambda destroyed diff --git a/tutorials/macros/10_capture_macro.das b/tutorials/macros/10_capture_macro.das new file mode 100644 index 0000000000..ff25f2ae70 --- /dev/null +++ b/tutorials/macros/10_capture_macro.das @@ -0,0 +1,126 @@ +options gen2 + +// Tutorial 10 — Capture macro +// +// Demonstrates AstCaptureMacro (registered via [capture_macro]). +// A capture macro hooks into lambda creation at three points: +// captureExpression — called per captured variable (can wrap/replace the value) +// captureFunction — called once per lambda (can add per-invocation cleanup) +// releaseFunction — called once per lambda finalizer (can add destruction cleanup) +// +// The companion module (capture_macro_mod) defines: +// [audited] — tag annotation for structs that should be logged +// CaptureAuditMacro — capture macro that logs capture, per-call, and release +// +// TIMING of each hook: +// captureExpression — fires at lambda CREATION (when the struct is built) +// captureFunction — adds code to the lambda function's body finalList, +// which runs after EACH INVOCATION (per-call finally). +// This is different from the user-written finally{} on the +// lambda literal, which runs on DESTRUCTION (once). +// releaseFunction — adds code to the lambda FINALIZER function's body, +// which runs on DESTRUCTION (after user-written finally{} +// but before field cleanup/delete). +// +// Non-[audited] types are silently ignored by the macro. + +require capture_macro_mod + +// ---- Struct definitions ----------------------------------------------------- + +[audited] +struct Resource { + name : string + id : int +} + +struct Plain { + x : int +} + +// ---- Section 1: basic capture auditing -------------------------------------- + +def section1() { + print("--- Section 1: basic capture auditing ---\n") + // Only the [audited] Resource triggers logging; Plain is silent. + // Variables are captured implicitly (by copy) when referenced in the lambda. + var res = Resource(name = "texture.png", id = 1) + var pl = Plain(x = 42) + var fn <- @() { + print(" body: res.name={res.name}, pl.x={pl.x}\n") + } + fn() + // captureExpression prints at creation (above). + // captureFunction's after-call message runs after fn() returns. + // releaseFunction's releasing message runs when fn is destroyed (below). + print(" about to delete fn...\n") + unsafe { delete fn; } + print("") +} + +// ---- Section 2: multiple [audited] fields ----------------------------------- + +def section2() { + print("\n--- Section 2: multiple [audited] fields ---\n") + // Each [audited] field produces its own capture and per-call messages. + var a = Resource(name = "mesh.obj", id = 2) + var b = Resource(name = "shader.hlsl", id = 3) + var fn <- @() { + print(" body: a.id={a.id}, b.id={b.id}\n") + } + fn() + fn() + // The after-call message appears twice — once per invocation. + // The releasing message appears once — on destruction. + print(" about to delete fn...\n") + unsafe { delete fn; } + print("") +} + +// ---- Section 3: non-audited types are silent -------------------------------- + +def section3() { + print("\n--- Section 3: non-audited types are silent ---\n") + var x = 10 + var y = 20 + var fn <- @() { + print(" body: x + y = {x + y}\n") + } + fn() + print(" about to delete fn...\n") + unsafe { delete fn; } + print(" (no audit messages for plain int captures)\n") +} + +[export] +def main() { + section1() + section2() + section3() +} + +// output: +// --- Section 1: basic capture auditing --- +// [audit] captured 'res' +// body: res.name=texture.png, pl.x=42 +// [audit] after-call: 'res' still captured +// about to delete fn... +// [audit] releasing 'res' +// +// --- Section 2: multiple [audited] fields --- +// [audit] captured 'a' +// [audit] captured 'b' +// body: a.id=2, b.id=3 +// [audit] after-call: 'a' still captured +// [audit] after-call: 'b' still captured +// body: a.id=2, b.id=3 +// [audit] after-call: 'a' still captured +// [audit] after-call: 'b' still captured +// about to delete fn... +// [audit] releasing 'a' +// [audit] releasing 'b' +// +// --- Section 3: non-audited types are silent --- +// body: x + y = 30 +// about to delete fn... +// (no audit messages for plain int captures) diff --git a/tutorials/macros/11_reader_macro.das b/tutorials/macros/11_reader_macro.das new file mode 100644 index 0000000000..57bb7682b7 --- /dev/null +++ b/tutorials/macros/11_reader_macro.das @@ -0,0 +1,87 @@ +options gen2 + +// Tutorial 11 — Reader macros +// +// Demonstrates AstReaderMacro (registered via [reader_macro]). +// A reader macro embeds custom syntax inside daScript source via the +// %name~ character_sequence %% syntax. The macro controls parsing +// through three methods: +// +// accept() — called per-character during parsing; collects input +// until the terminator (%%) is reached. +// visit() — called during type inference; replaces the ExprReader +// node with an AST expression. Used for EXPRESSION-LEVEL +// reader macros (the "visit" pattern). +// suffix() — called immediately after accept() during parsing; +// returns daScript source text that the parser re-parses. +// Used for MODULE-LEVEL reader macros (the "suffix" pattern). +// +// The companion module (reader_macro_mod) defines: +// CsvReader — [reader_macro(name=csv)] visit pattern +// BasicReader — [reader_macro(name=basic)] suffix pattern +// +// Section 1 shows the visit pattern: %csv~ embeds a compile-time +// string array from comma-separated text. +// Section 2 shows the suffix pattern: %basic~ transpiles a toy BASIC +// program into a daScript function at parse time. + +require reader_macro_mod + +// ---- Section 1: CSV reader (visit pattern) ---------------------------------- + +def section1() { + print("--- Section 1: CSV reader (visit pattern) ---\n") + // %csv~ ... %% parses CSV at compile time into a string array. + // The accept() method collects characters; visit() splits by comma, + // trims whitespace, and builds the array via convert_to_expression(). + var data <- %csv~ Alice, 30, New York %% + print(" items ({length(data)}):\n") + for (item in data) { + print(" '{item}'\n") + } + // A second example with different data. + var colors <- %csv~red,green,blue%% + print(" colors ({length(colors)}):\n") + for (c in colors) { + print(" '{c}'\n") + } + delete data + delete colors +} + +// ---- Section 2: BASIC transpiler (suffix pattern) --------------------------- +// The %basic~ ... %% reader macro at MODULE LEVEL generates a daScript +// function definition. The suffix() method transpiles numbered BASIC +// lines into daScript source code, which the parser re-parses as normal +// module-level content. The ExprReader node is discarded. +// +// The generated function can then be called from daScript code. + +%basic~ +DEF basic_hello +10 PRINT "Hello from BASIC" +20 LET x = 42 +30 PRINT x +%% + +[export] +def main() { + section1() + print("\n--- Section 2: BASIC transpiler (suffix pattern) ---\n") + basic_hello() +} + +// output: +// --- Section 1: CSV reader (visit pattern) --- +// items (3): +// 'Alice' +// '30' +// 'New York' +// colors (3): +// 'red' +// 'green' +// 'blue' +// +// --- Section 2: BASIC transpiler (suffix pattern) --- +// Hello from BASIC +// 42 diff --git a/tutorials/macros/12_typeinfo_macro.das b/tutorials/macros/12_typeinfo_macro.das new file mode 100644 index 0000000000..3ffbcbcd5c --- /dev/null +++ b/tutorials/macros/12_typeinfo_macro.das @@ -0,0 +1,133 @@ +options gen2 + +// Tutorial 12 — Typeinfo macros +// +// Demonstrates AstTypeInfoMacro (registered via [typeinfo_macro]). +// A typeinfo macro extends the built-in typeinfo(...) expression with +// custom compile-time type introspection. The macro's getAstChange +// method receives an ExprTypeInfo node and returns a replacement AST +// expression — typically a constant (string, int, bool, array) that +// the compiler folds into the program. +// +// The companion module (typeinfo_macro_mod) defines: +// struct_info — returns a string describing a struct +// enum_value_strings — returns an array of enum names +// has_non_static_method — returns bool via subtrait parameter +// +// Section 1 shows struct_info: compile-time struct description. +// Section 2 shows enum_value_strings: compile-time enum names array. +// Section 3 shows has_non_static_method: compile-time method check. + +require typeinfo_macro_mod + +// ---- Section 1: struct_info ------------------------------------------------- + +struct Vec3 { + x : float + y : float + z : float +} + +struct Person { + name : string + age : int +} + +def section1() { + print("--- Section 1: struct_info ---\n") + // typeinfo struct_info(type) is resolved entirely at compile time. + // The macro builds a string like "Name(field:type, ...)" from the + // struct's field declarations. + let v = typeinfo struct_info(type) + print(" Vec3: {v}\n") + let p = typeinfo struct_info(type) + print(" Person: {p}\n") +} + +// ---- Section 2: enum_value_strings ------------------------------------------ + +enum Color { + Red + Green + Blue +} + +enum Direction { + North + South + East + West +} + +def section2() { + print("--- Section 2: enum_value_strings ---\n") + // typeinfo enum_value_strings(type) returns an array of strings + // with all enum value names, built at compile time. + let colors = typeinfo enum_value_strings(type) + print(" Color values ({length(colors)}):\n") + for (c in colors) { + print(" {c}\n") + } + let dirs = typeinfo enum_value_strings(type) + print(" Direction values ({length(dirs)}):\n") + for (d in dirs) { + print(" {d}\n") + } +} + +// ---- Section 3: has_non_static_method --------------------------------------- + +class Animal { + name : string + def speak() { + print(" {name} says hello\n") + } +} + +class Rock { + weight : float +} + +def section3() { + print("--- Section 3: has_non_static_method ---\n") + // typeinfo has_non_static_method(type) uses the subtrait + // (the parameter) to check whether a class has a non-static + // method with that name. + let animal_can_speak = typeinfo has_non_static_method < speak > (type) + print(" Animal has 'speak': {animal_can_speak}\n") + let animal_can_fly = typeinfo has_non_static_method < fly > (type) + print(" Animal has 'fly': {animal_can_fly}\n") + let rock_can_speak = typeinfo has_non_static_method < speak > (type) + print(" Rock has 'speak': {rock_can_speak}\n") + // Compile-time boolean — usable in static_if for conditional code. + static_if (typeinfo has_non_static_method < speak > (type)) { + print(" (static_if confirmed: Animal can speak)\n") + } +} + +// ---- expected output -------------------------------------------------------- +// --- Section 1: struct_info --- +// Vec3: Vec3(x:float, y:float, z:float) +// Person: Person(name:string, age:int) +// --- Section 2: enum_value_strings --- +// Color values (3): +// Red +// Green +// Blue +// Direction values (4): +// North +// South +// East +// West +// --- Section 3: has_non_static_method --- +// Animal has 'speak': true +// Animal has 'fly': false +// Rock has 'speak': false +// (static_if confirmed: Animal can speak) + +[export] +def main() { + section1() + section2() + section3() +} diff --git a/tutorials/macros/13_enumeration_macro.das b/tutorials/macros/13_enumeration_macro.das new file mode 100644 index 0000000000..a97db46869 --- /dev/null +++ b/tutorials/macros/13_enumeration_macro.das @@ -0,0 +1,79 @@ +options gen2 + +// Tutorial 13 — Enumeration macros +// +// Demonstrates AstEnumerationAnnotation (registered via [enumeration_macro]). +// An enumeration annotation has a single method: +// apply(var enu, var group, args, var errors) : bool +// Called BEFORE the infer pass — the macro can modify the enum or +// generate new code (functions, global variables) in the compiling module. +// +// Section 1 uses [enum_total] from enum_macro_mod — modifies the enum +// by adding a "total" entry equal to the number of original values. +// Section 2 uses [string_to_enum] from daslib/enum_trait — generates +// a lookup table and two constructor functions for string-to-enum +// conversion. + +require enum_macro_mod +require daslib/enum_trait + +// ---- Section 1: enum_total — modifying the enum ---------------------------- + +[enum_total] +enum Direction { + North + South + East + West +} + +def section1() { + // Direction now has an extra entry: Direction.total == 4. + print("--- Section 1: enum_total ---\n") + print("Direction.total = {int(Direction.total)}\n") + // Iterate all original values using each() from enum_trait. + // each() yields every value including total, so we stop at total. + for (d in each(Direction.North)) { + if (d == Direction.total) { + break + } + print(" {d}\n") + } +} + +// ---- Section 2: string_to_enum — generating code --------------------------- + +[string_to_enum] +enum Color { + Red + Green + Blue +} + +def section2() { + // [string_to_enum] generated two functions: + // Color(src : string) : Color — panics if not found + // Color(src : string; def : Color) : Color — returns default if not found + print("--- Section 2: string_to_enum ---\n") + let c1 = Color("Red") + print("Color(\"Red\") = {c1}\n") + let c2 = Color("invalid", Color.Blue) + print("Color(\"invalid\", Color.Blue) = {c2}\n") +} + +[export] +def main() { + section1() + section2() +} + +// Expected output: +// --- Section 1: enum_total --- +// Direction.total = 4 +// North +// South +// East +// West +// --- Section 2: string_to_enum --- +// Color("Red") = Red +// Color("invalid", Color.Blue) = Blue diff --git a/tutorials/macros/14_pass_macro.das b/tutorials/macros/14_pass_macro.das new file mode 100644 index 0000000000..accb1774ca --- /dev/null +++ b/tutorials/macros/14_pass_macro.das @@ -0,0 +1,60 @@ +options gen2 + +// Tutorial 14 — Pass macro +// +// Demonstrates AstPassMacro — whole-program macros invoked at different +// compilation passes. Two annotations are shown: +// +// [lint_macro] (Section 1) — invoked for each module compiled after the +// macro module. The lint pass is read-only: it can inspect the +// fully-typed AST and report diagnostics but cannot modify code. +// The macro uses compiling_module() to inspect the current module +// and warns about functions whose body exceeds a statement threshold. +// +// [infer_macro] (Section 2) — runs during the inference pass. The macro +// can walk and modify the AST. Returning true from apply() tells the +// compiler to re-infer; the loop continues until every macro returns +// false. The macro in pass_macro_mod inserts a _trace_enter() call at +// every function entry, following the pattern used by daslib/heartbeat. + +require pass_macro_mod + +// ---- helper functions for demonstration ------------------------------------ + +def greet(name : string) { + print("Hello, {name}!\n") +} + +def sum_to(n : int) : int { + var total = 0 + for (i in range(n)) { + total += i + } + return total +} + +def countdown(n : int) { + var i = n + while (i > 0) { + i-- + } + print("counted down from {n}\n") +} + +[export] +def main() { + greet("world") + let s = sum_to(5) + print("sum = {s}\n") + countdown(3) +} + +// Expected output: +// [lint] 'main' has 5 top-level statements (>4) +// >>> main +// >>> greet +// Hello, world! +// >>> sum_to +// sum = 10 +// >>> countdown +// counted down from 3 diff --git a/tutorials/macros/capture_macro_mod.das b/tutorials/macros/capture_macro_mod.das new file mode 100644 index 0000000000..d1faeaf81e --- /dev/null +++ b/tutorials/macros/capture_macro_mod.das @@ -0,0 +1,140 @@ +options gen2 +options no_aot + +// Tutorial macro module: capture macro that audits captured fields. +// +// Provides: +// [audited] — structure annotation (tag) marking a struct for audit +// CaptureAuditMacro — [capture_macro] with three hooks: +// captureExpression — wraps capture value in audit_on_capture() (per variable) +// captureFunction — appends audit_after_invoke() to per-call finally +// releaseFunction — appends audit_on_finalize() to the finalizer (destruction) +// audit_on_capture() — runtime helper: prints a message and returns the captured value +// audit_after_invoke() — runtime helper: prints a message after each lambda invocation +// audit_on_finalize() — runtime helper: prints a message when a lambda is destroyed + +module capture_macro_mod + +require ast +require daslib/ast_boost + +// ---- [audited] tag annotation ----------------------------------------------- +// Mark a struct with [audited] to opt in to capture/release logging. +// The annotation itself does nothing — the capture macro checks for it. + +[structure_macro(name=audited)] +class AuditedAnnotation : AstStructureAnnotation { + def override apply(var st : StructurePtr; var group : ModuleGroup; args : AnnotationArgumentList; var errors : das_string) : bool { + return true // no-op tag + } +} + +// ---- Runtime helpers -------------------------------------------------------- + +def audit_on_capture(value : auto(T); field_name : string) : T { + //! Prints a capture-audit message and returns the value unchanged. + print("[audit] captured '{field_name}'\n") + return value +} + +def audit_after_invoke(field_name : string) { + //! Prints a post-invocation audit message. + //! Called from the lambda function's finalList (runs after each invocation, + //! NOT on destruction — see captureFunction below). + print("[audit] after-call: '{field_name}' still captured\n") +} + +def audit_on_finalize(field_name : string) { + //! Prints a release-audit message when the lambda is destroyed. + //! Called from the lambda FINALIZER body (runs once on destruction, + //! after the user-written finally{} block but before field cleanup). + print("[audit] releasing '{field_name}'\n") +} + +// ---- Type check helper ------------------------------------------------------ + +[macro_function] +def private is_audited(typ : TypeDeclPtr) : bool { + if (!typ.isStructure || typ.structType == null) { + return false + } + for (ann in typ.structType.annotations) { + if (ann.annotation.name == "audited") { + return true + } + } + return false +} + +// ---- Capture macro ---------------------------------------------------------- + +[capture_macro(name=capture_audit)] +class CaptureAuditMacro : AstCaptureMacro { + //! Audits capture and release of [audited] struct fields in lambdas. + //! + //! captureExpression — wraps the capture expression in audit_on_capture(), + //! which prints a message and passes the value through. + //! captureFunction — appends audit_after_invoke() calls to the lambda + //! function's body finalList for each [audited] field. This code runs + //! after EACH invocation (per-call finally), not on destruction. + //! releaseFunction — appends audit_on_finalize() calls to the lambda + //! FINALIZER body for each [audited] field. This code runs once on + //! destruction (after the user-written finally{} but before field cleanup). + + def override captureExpression(prog : Program?; mod : Module?; expr : ExpressionPtr; etype : TypeDeclPtr) : ExpressionPtr { + if (is_in_completion()) { + return <- default + } + if (!is_audited(etype)) { + return <- default + } + // Determine the field name from the expression (typically an ExprVar) + var field_name = "unknown" + if (expr is ExprVar) { + field_name = string((expr as ExprVar).name) + } + // Wrap: capture_macro_mod::audit_on_capture(original_expr, "field_name") + var inscope pCall <- new ExprCall(at = expr.at, name := "capture_macro_mod::audit_on_capture") + pCall.arguments |> emplace_new <| clone_expression(expr) + pCall.arguments |> emplace_new <| new ExprConstString(at = expr.at, value := field_name) + return <- pCall + } + + def override captureFunction(prog : Program?; mod : Module?; var lcs : Structure?; var fun : FunctionPtr) : void { + // Generators call finally on every iteration — skip them + if (fun.flags._generator) { + return + } + for (fld in lcs.fields) { + if (!is_audited(fld._type)) { + continue + } + // Append: capture_macro_mod::audit_after_invoke("field_name") + // NOTE: this adds to the lambda FUNCTION's body finalList, so it + // runs after each invocation — like a per-call finally. This is + // different from the user-written finally{} block on the lambda + // literal, which goes into the FINALIZER and runs on destruction. + if (true) { + var inscope pCall <- new ExprCall(at = fld.at, name := "capture_macro_mod::audit_after_invoke") + pCall.arguments |> emplace_new <| new ExprConstString(at = fld.at, value := string(fld.name)) + (fun.body as ExprBlock).finalList |> emplace(pCall) + } + } + } + + def override releaseFunction(prog : Program?; mod : Module?; var lcs : Structure?; var fun : FunctionPtr) : void { + // Called once per lambda FINALIZER function. Code appended here + // runs on DESTRUCTION — after the user-written finally{} block but + // before the compiler-generated field cleanup (delete *__this). + for (fld in lcs.fields) { + if (!is_audited(fld._type)) { + continue + } + if (true) { + var inscope pCall <- new ExprCall(at = fld.at, name := "capture_macro_mod::audit_on_finalize") + pCall.arguments |> emplace_new <| new ExprConstString(at = fld.at, value := string(fld.name)) + (fun.body as ExprBlock).list |> emplace(pCall) + } + } + } +} diff --git a/tutorials/macros/enum_macro_mod.das b/tutorials/macros/enum_macro_mod.das new file mode 100644 index 0000000000..40995d95b4 --- /dev/null +++ b/tutorials/macros/enum_macro_mod.das @@ -0,0 +1,42 @@ +options gen2 +options no_aot + +// Tutorial macro module: enumeration macro (AstEnumerationAnnotation). +// +// Provides [enum_total] — an enumeration annotation that adds a "total" +// entry to any enum, set to the number of original values. +// +// AstEnumerationAnnotation has a single method: +// apply(var st, var group, args, var errors) : bool +// Called BEFORE the infer pass — can modify the enum or generate code. +// +// This macro demonstrates modifying the enum itself by adding a new +// entry with add_enumeration_entry. + +module enum_macro_mod + +require ast +require daslib/ast_boost + +[enumeration_macro(name="enum_total")] +class EnumTotalAnnotation : AstEnumerationAnnotation { + def override apply(var enu : EnumerationPtr; var group : ModuleGroup; args : AnnotationArgumentList; var errors : das_string) : bool { + // Check that the enum doesn't already have a "total" entry. + for (ee in enu.list) { + if (ee.name == "total") { + errors := "enumeration already has a 'total' field" + return false + } + } + // Add a new "total" entry at the end. + let idx = add_enumeration_entry(enu, "total") + if (idx < 0) { + errors := "failed to add 'total' field" + return false + } + // Set total = number of original entries (before we added "total"). + // length(enu.list) is now N+1, so total = N+1-1 = N. + enu.list[idx].value |> move_new() <| new ExprConstInt(at = enu.at, value = length(enu.list) - 1) + return true + } +} diff --git a/tutorials/macros/pass_macro_mod.das b/tutorials/macros/pass_macro_mod.das new file mode 100644 index 0000000000..6de9c8264a --- /dev/null +++ b/tutorials/macros/pass_macro_mod.das @@ -0,0 +1,114 @@ +options gen2 +options no_aot + +// Tutorial macro module: pass macro (AstPassMacro). +// +// AstPassMacro is a whole-program macro invoked during compilation. +// It has a single method: +// apply(prog : ProgramPtr; mod : Module?) : bool +// Five annotations control WHEN the macro runs: +// [infer_macro] — after clean inference (returning true re-infers) +// [dirty_infer_macro] — during each dirty inference pass +// [lint_macro] — after each module is compiled (per-module) +// [global_lint_macro] — once after the entire program is compiled +// [optimization_macro] — during optimization passes +// +// Section 1 — [lint_macro]: invoked for each module compiled after this +// macro module. Uses `compiling_module()` to inspect the current +// module. Read-only — cannot modify the AST. +// +// Section 2 — [infer_macro]: instruments every function body with a +// _trace_enter() call, following the heartbeat.das pattern. +// Returns true when changes are made so the compiler re-infers. + +module pass_macro_mod + +require ast +require daslib/ast_boost +require daslib/templates_boost + +// ---- Section 1: lint_macro — per-module analysis --------------------------- + +[lint_macro] +class CodeStatsLint : AstPassMacro { + //! Lint pass invoked for each module compiled after this one. + //! Uses compiling_module() to inspect the current module only. + def override apply(prog : ProgramPtr; mod : Module?) : bool { + // compiling_module() returns the module being compiled right now. + // This is NOT the same as `mod`, which is the module that owns + // this macro (pass_macro_mod). + let cm = compiling_module() + let WARN_THRESHOLD = 4 + cm |> for_each_function("") <| $(var func : FunctionPtr) { + if (func.body == null || !(func.body is ExprBlock)) { + return + } + let body = func.body as ExprBlock + let nStmts = length(body.list) + if (nStmts > WARN_THRESHOLD) { + print("[lint] '{func.name}' has {nStmts} top-level statements (>{WARN_THRESHOLD})\n") + } + } + return false // lint macros don't modify the AST + } +} + +// ---- Section 2: infer_macro — AST transformation --------------------------- + +def public _trace_enter(name : string) { + //! Prints a trace message when entering a function. + print(">>> {name}\n") +} + +class TraceCallsVisitor : AstVisitor { + //! AST visitor that inserts _trace_enter() at the start of every function. + astChanged : bool = false + @do_not_delete func : Function? + def override preVisitFunction(var fun : FunctionPtr) { + func = get_ptr(fun) + } + def override visitFunction(var fun : FunctionPtr) : FunctionPtr { + // Skip our own helper to avoid infinite recursion at runtime. + if (string(fun.name) == "_trace_enter") { + func = null + return <- fun + } + if (fun.body == null || !(fun.body is ExprBlock)) { + func = null + return <- fun + } + var body = fun.body as ExprBlock + if (length(body.list) == 0) { + func = null + return <- fun + } + // Idempotency: skip if already instrumented. + if ((body.list[0] is ExprCall) && (body.list[0] as ExprCall).name == "_trace_enter") { + func = null + return <- fun + } + // Insert _trace_enter("function_name") at the beginning of the body. + let fname = string(fun.name) + var inscope expr <- qmacro(_trace_enter($v(fname))) + body.list |> emplace(expr, 0) + astChanged = true + func.not_inferred() + func = null + return <- fun + } +} + +[infer_macro] +class TraceCallsPass : AstPassMacro { + //! Pass macro that inserts trace calls at every function entry. + def override apply(prog : ProgramPtr; mod : Module?) : bool { + var astVisitor = new TraceCallsVisitor() + var inscope astVisitorAdapter <- make_visitor(*astVisitor) + visit(prog, astVisitorAdapter) + var result = astVisitor.astChanged + unsafe { + delete astVisitor + } + return result + } +} diff --git a/tutorials/macros/reader_macro_mod.das b/tutorials/macros/reader_macro_mod.das new file mode 100644 index 0000000000..dd9187576e --- /dev/null +++ b/tutorials/macros/reader_macro_mod.das @@ -0,0 +1,161 @@ +options gen2 +options no_aot + +// Tutorial macro module: reader macros (visit + suffix patterns). +// +// Provides two AstReaderMacro implementations: +// CsvReader — [reader_macro(name=csv)] visit pattern +// Parses CSV text at compile time, returns an array expression. +// BasicReader — [reader_macro(name=basic)] suffix pattern +// Transpiles BASIC-style code into daScript source, which +// the parser re-parses as normal code. +// +// TIMING: +// accept() — called character-by-character during PARSING +// suffix() — called immediately after accept() during PARSING; +// returns source text injected into the lexer +// visit() — called during TYPE INFERENCE on the ExprReader node; +// returns an AST expression replacing the reader expression +// +// Visit pattern (CsvReader): +// accept() collects characters → visit() parses at compile time → returns AST +// Used as an EXPRESSION: var data = %csv~ ... %% +// +// Suffix pattern (BasicReader): +// accept() collects characters → suffix() returns daScript source → parser re-parses +// Used at MODULE LEVEL: %basic~ ... %% +// The ExprReader node is discarded; only the injected text matters. + +module reader_macro_mod + +require ast +require strings +require daslib/ast_boost +require daslib/strings_boost + +// ---- CSV reader (visit pattern) --------------------------------------------- +// %csv~ Alice,30,New York %% → array of trimmed strings at compile time. + +[reader_macro(name=csv)] +class CsvReader : AstReaderMacro { + //! Embeds a CSV literal as a string array at compile time. + //! + //! accept() — collects characters until the %% terminator. + //! visit() — splits the sequence by commas, trims whitespace, + //! and returns the result via convert_to_expression(). + + def override accept(prog : ProgramPtr; mod : Module?; var expr : ExprReader?; ch : int; info : LineInfo) : bool { + //! Standard %% terminator idiom: append each character, + //! check for the terminator, strip it, and return false. + if (ch != '\r') { + append(expr.sequence, ch) + } + if (ends_with(expr.sequence, "%%")) { + let len = length(expr.sequence) + resize(expr.sequence, len - 2) + return false + } else { + return true + } + } + + def override visit(prog : ProgramPtr; mod : Module?; expr : smart_ptr) : ExpressionPtr { + //! Parse the collected CSV text at compile time and embed + //! the resulting string array directly in the AST. + if (is_in_completion()) { + return <- default + } + let seq = string(expr.sequence) + var items <- split(seq, ",") + for (i in range(length(items))) { + items[i] = strip(items[i]) + } + // convert_to_expression turns any daScript value into AST nodes. + // Here it produces an ExprMakeArray of ExprConstString values. + return <- convert_to_expression(items, expr.at) + } +} + +// ---- BASIC reader (suffix pattern) ------------------------------------------ +// %basic~ DEF hello \n 10 PRINT "Hello" \n 20 LET x = 42 \n 30 PRINT x %% +// Generates a daScript function definition that the parser re-parses. + +[reader_macro(name=basic)] +class BasicReader : AstReaderMacro { + //! Transpiles a tiny BASIC program into a daScript function. + //! + //! accept() — collects characters until the %% terminator. + //! suffix() — parses numbered BASIC lines, generates a daScript + //! function definition, and returns it as source text that the + //! parser re-parses. + //! + //! Supported BASIC commands: + //! DEF name — names the generated function (must be first) + //! NUMBER PRINT "s" — print a string literal + //! NUMBER PRINT var — print a variable via string interpolation + //! NUMBER LET v = e — declare a variable + + def override accept(prog : ProgramPtr; mod : Module?; var expr : ExprReader?; ch : int; info : LineInfo) : bool { + if (ch != '\r') { + append(expr.sequence, ch) + } + if (ends_with(expr.sequence, "%%")) { + let len = length(expr.sequence) + resize(expr.sequence, len - 2) + return false + } else { + return true + } + } + + def override suffix(prog : ProgramPtr; mod : Module?; var expr : ExprReader?; info : LineInfo; var outLine : int&; var outFile : FileInfo?&) : string { + let seq = string(expr.sequence) + var lines <- split(seq, "\n") + var func_name = "basic_program" + var stmts : array + for (line in lines) { + let trimmed = strip(line) + if (empty(trimmed)) { + continue + } + // DEF name — must appear first + if (starts_with(trimmed, "DEF ")) { + func_name = strip(slice(trimmed, 4)) + continue + } + // Numbered lines: skip the number, parse COMMAND args + // Find first space (after the line number) + let sp1 = find(trimmed, " ") + if (sp1 < 0) { + continue + } + let after_num = strip(slice(trimmed, sp1 + 1)) + if (starts_with(after_num, "PRINT ")) { + let arg = strip(slice(after_num, 6)) + if (starts_with(arg, "\"")) { + // String literal: PRINT "text" + // Strip quotes, wrap in print("...\n") + let inner = slice(arg, 1, length(arg) - 1) + stmts |> push("print(\"{inner}\\n\")") + } else { + // Variable: PRINT x → print("{x}\n") + // In the suffix output we need literal {x} for the parser, + // so we use escaped braces \{ and \} around the interpolation. + stmts |> push("print(\"\{{arg}\}\\n\")") + } + } elif (starts_with(after_num, "LET ")) { + // LET x = 42 → var x = 42 + let assignment = strip(slice(after_num, 4)) + stmts |> push("var {assignment}") + } + } + // Build the daScript function definition (gen2 syntax — braces required). + // \{ and \} produce literal braces in the output, avoiding interpolation. + var result = "def {func_name}() \{\n" + for (stmt in stmts) { + result += " {stmt}\n" + } + result += "\}\n" + return result + } +} diff --git a/tutorials/macros/typeinfo_macro_mod.das b/tutorials/macros/typeinfo_macro_mod.das new file mode 100644 index 0000000000..a4ca711e49 --- /dev/null +++ b/tutorials/macros/typeinfo_macro_mod.das @@ -0,0 +1,128 @@ +options gen2 +options no_aot + +// Tutorial macro module: typeinfo macros (AstTypeInfoMacro). +// +// Provides three [typeinfo_macro] implementations demonstrating +// getAstChange — compile-time type introspection that replaces a +// typeinfo(...) expression with a constant AST node. +// +// struct_info — returns a string describing a struct's fields +// enum_value_strings — returns an array of enum value names +// has_non_static_method — returns true/false whether a class has a method +// +// Each macro receives an ExprTypeInfo node from the compiler: +// expr.typeexpr — the TypeDeclPtr for the type argument +// expr.subtrait — the in typeinfo trait(...) +// expr.at — source location for generated nodes + +module typeinfo_macro_mod + +require ast +require strings +require daslib/ast_boost + +// --------------------------------------------------------------------------- +// Section 1: struct_info +// +// typeinfo struct_info(type) → string +// Returns "StructName(field1:type1, field2:type2, ...)" at compile time. +// --------------------------------------------------------------------------- + +[typeinfo_macro(name="struct_info")] +class TypeInfoGetStructInfo : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; var errors : das_string) : ExpressionPtr { + if (expr.typeexpr == null) { + errors := "type is missing or not inferred" + return <- default + } + if (!expr.typeexpr.isStructure) { + errors := "expecting structure type" + return <- default + } + // Build "Name(f1:t1, f2:t2, ...)" string. + var result = "{expr.typeexpr.structType.name}(" + var first = true + for (i in iter_range(expr.typeexpr.structType.fields)) { + assume fld = expr.typeexpr.structType.fields[i] + if (fld.flags.classMethod) { + continue // skip methods — show only data fields + } + if (!first) { + result += ", " + } + result += "{fld.name}:{describe(fld._type, false, false, false)}" + first = false + } + result += ")" + return <- new ExprConstString(at = expr.at, value := result) + } +} + +// --------------------------------------------------------------------------- +// Section 2: enum_value_strings +// +// typeinfo enum_value_strings(type) → array +// Returns an array of enum value name strings at compile time. +// --------------------------------------------------------------------------- + +[typeinfo_macro(name="enum_value_strings")] +class TypeInfoGetEnumValueStrings : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; var errors : das_string) : ExpressionPtr { + if (expr.typeexpr == null) { + errors := "type is missing or not inferred" + return <- default + } + if (!expr.typeexpr.isEnum) { + errors := "expecting enumeration type" + return <- default + } + // Build ExprMakeArray of ExprConstString entries. + var inscope arr <- new ExprMakeArray(at = expr.at, makeType <- typeinfo ast_typedecl(type)) + for (i in iter_range(expr.typeexpr.enumType.list)) { + if (true) { + assume entry = expr.typeexpr.enumType.list[i] + var inscope nameExpr <- new ExprConstString(at = expr.at, value := entry.name) + arr.values |> emplace <| nameExpr + } + } + return <- arr + } +} + +// --------------------------------------------------------------------------- +// Section 3: has_non_static_method +// +// typeinfo has_non_static_method(type) → bool +// Returns true if the struct/class has a non-static method with the given +// name. Uses the subtrait to get the method name, and checks the struct's +// fields for a classMethod flag. +// --------------------------------------------------------------------------- + +[typeinfo_macro(name="has_non_static_method")] +class TypeInfoHasNonStaticMethod : AstTypeInfoMacro { + def override getAstChange(expr : smart_ptr; var errors : das_string) : ExpressionPtr { + if (expr.typeexpr == null) { + errors := "type is missing or not inferred" + return <- default + } + if (!expr.typeexpr.isStructure) { + errors := "expecting structure or class type" + return <- default + } + if (empty(expr.subtrait)) { + errors := "expecting method name as subtrait: typeinfo has_non_static_method(type)" + return <- default + } + // Check if any field with the given name has classMethod flag. + var found = false + for (i in iter_range(expr.typeexpr.structType.fields)) { + assume fld = expr.typeexpr.structType.fields[i] + if (fld.name == expr.subtrait && fld.flags.classMethod) { + found = true + break + } + } + return <- new ExprConstBool(at = expr.at, value = found) + } +}